Skip to content

Connect with Webhooks or the Developer API

CopyLoop provides two ways to send customer data through one shared /v1 architecture:

UseBest forWhat it sendsList routing
WebhookZapier, Make, n8n, form builders, and other POST-capable toolsContactsOne list selected in CopyLoop
Developer APIA trusted backend, serverless function, or durable workerContact identity, product events, and consent evidenceEach identify request may name different lists
Webhook
Best for
Zapier, Make, n8n, form builders, and other POST-capable tools
What it sends
Contacts
List routing
One list selected in CopyLoop
Developer API
Best for
A trusted backend, serverless function, or durable worker
What it sends
Contact identity, product events, and consent evidence
List routing
Each identify request may name different lists

Both return durable receipts. They intentionally use different edge credentials and payloads: a Webhook secret cannot call Developer API operations, and a Developer API key cannot submit to a Webhook URL.

Accepted data provides evidence and potential automation candidates. It does not give Loop or an automation authority to send email, publish content, or take another external action. CopyLoop still applies capability, approval, consent, suppression, and final-authority checks.

The OpenAPI document is the authoritative v1 contract.

  1. In CopyLoop, open Settings → Integrations.
  2. Select Webhooks for an automation tool or Developer API for application code.
  3. Give the connection a name that identifies the system sending data, such as Customer portal or Signup automation.
  4. For a Webhook, select its destination contact list. For the Developer API, create any contact lists your application will use.
  5. Create the connection and save the displayed secret immediately. It is shown only once.

Customer-created connections send to the production workspace. There is no environment selector in the request or setup form. Use a controlled email address and a clearly named list for verification.

Keep every secret in protected automation-tool connection settings or a server-side secret manager. Never put one in browser JavaScript, page HTML, a URL, a public repository, ordinary logs, analytics, or a distributed mobile application.

Choose a Webhook when a tool should send contacts without implementing the strict Developer API contract.

Save the generated URL and secret in the automation platform’s protected connection settings:

Terminal window
COPYLOOP_WEBHOOK_URL=https://api.copyloop.com/v1/webhooks/<key-id>
COPYLOOP_WEBHOOK_SECRET=<secret-shown-once>

Send JSON or form-encoded data to the generated URL:

Terminal window
curl --fail-with-body \
--request POST "$COPYLOOP_WEBHOOK_URL" \
--header "X-API-Key: $COPYLOOP_WEBHOOK_SECRET" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: signup:customer_123" \
--data '{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"company": "Acme",
"job_title": "VP Marketing"
}'

Only a valid email is required. Webhook v1 recognizes these standard fields:

CopyLoop field Accepted webhook names
Email email, email_address, emailAddress
First name firstName, first_name
Last name lastName, last_name
Company company, company_name, organization
Title title, job_title
Phone phone, phone_number
Mobile phone mobilePhone, mobile_phone, mobile
City city
State or region state, region, province
Postal code postalCode, postal_code, zip
Country country
Timezone timezone, time_zone

Unknown fields are ignored. They are not automatically created as custom fields. Use the Developer API when you need explicit source identity, product events, or consent evidence.

A successful Webhook submission returns 202 Accepted and a receipt. When the sending tool has a stable delivery ID, send it as Idempotency-Key:

  • an identical retry returns the same receipt with admissionDisposition: "transport_replay"
  • reusing the key with a different accepted contact value returns 409 idempotency_conflict
  • ignored fields and CopyLoop-generated observation metadata do not affect retry equality
  • without a key, contact state still converges by normalized email, but retries can create multiple receipts

The Webhook always sends the contact to the list selected during setup. Change that destination in CopyLoop rather than adding a list ID to the Webhook payload.

Choose the Developer API when you control a trusted backend and need a versioned contract.

Set these server-side environment variables:

Terminal window
COPYLOOP_API_BASE_URL=https://api.copyloop.com
COPYLOOP_APPLICATION_SOURCE_KEY=cl_src_<key-id>_<secret>

Use one API connection per application or sending system. The key is not tied to a contact list: the same key can send different contacts to different lists.

Every write requires:

  • Authorization: Bearer <credential>
  • Content-Type: application/json
  • an opaque Idempotency-Key of at most 200 characters

Use stable, non-personal identifiers. Do not put an email address or other personal data in a source user ID, idempotency key, observation ID, event ID, or consent decision ID.

Read the active contact lists available to this API connection:

Terminal window
curl --fail-with-body \
--header "Authorization: Bearer $COPYLOOP_APPLICATION_SOURCE_KEY" \
"$COPYLOOP_API_BASE_URL/v1/contact-lists"
{
"contactLists": [
{
"id": "0198ec8e-3210-7abc-9123-0123456789ab",
"name": "Product updates",
"brand": {
"id": "0198ec8e-3210-7abc-9123-0123456789ac",
"name": "Acme"
}
},
{
"id": "0198ec8e-3210-7abc-9123-0123456789ad",
"name": "Workspace newsletter",
"brand": null
}
]
}

Persist or configure the stable id, not the display name. Names can change and need not be unique. The optional brand helps distinguish similarly named lists.

This endpoint does not return contacts, membership counts, deleted lists, or credentials. An API key also cannot create, rename, or delete lists. If a destination is missing, a workspace owner creates it in Contacts → Lists, then the application reads this endpoint again.

Send POST /v1/contacts/identify when a user is created or supported profile data changes.

CopyLoop contacts are email-addressable recipients. Email is required and is the canonical cross-source match key within a workspace. An application user and a Salesforce record with the same normalized email can therefore resolve to one CopyLoop contact. The application user ID remains a durable source alias for later signals.

This is a complete routine request:

Terminal window
curl --fail-with-body \
--request POST "$COPYLOOP_API_BASE_URL/v1/contacts/identify" \
--header "Authorization: Bearer $COPYLOOP_APPLICATION_SOURCE_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: identify:obs_user_123_revision_42" \
--data '{
"observationId": "obs_user_123_revision_42",
"subject": {
"type": "user",
"id": "user_123"
},
"observedAt": "2026-07-27T12:00:00.000Z",
"email": {
"address": "jane@example.com"
},
"traits": {
"firstName": "Jane",
"lastName": "Doe",
"company": "Acme"
},
"addToListIds": [
"0198ec8e-3210-7abc-9123-0123456789ab"
]
}'

Only observationId, subject, observedAt, and email are required. CopyLoop applies these routine defaults:

Field Default
traitSchemaVersion 1
mode patch
state active
traits {}
unsetTraits []

Omitting a default and sending it explicitly have the same domain meaning.

addToListIds is optional and add-only:

  • include up to 25 IDs returned by GET /v1/contact-lists
  • use different IDs on different requests with the same API key
  • omit it when the identify should not change list membership
  • one invalid, deleted, or different-workspace ID rejects the complete request before CopyLoop creates the contact or receipt

Identify never removes an unlisted membership, restores an unsubscribed membership, records consent, or clears suppression.

Trait schema version 1 accepts:

Category Fields
Standard contact firstName, lastName, company, title, phone, mobilePhone, city, state, postalCode, country, timezone
Application context plan, signedUpAt

Standard contact traits can fill an empty CopyLoop contact field. The concrete application subject then owns that field and may update or unset it. An existing Salesforce-projected or manually managed value is not overwritten. If Salesforce or a manual edit takes over later, the Application Source relinquishes ownership instead of overwriting that value.

plan and signedUpAt remain source-owned context. They do not silently create workspace custom fields.

The schema is closed intentionally. Unknown keys return 422 rather than becoming untyped, unauditable data. Custom traits need an explicit field definition so CopyLoop can preserve their type, provenance, rights, deletion, segmentation, and personalization behavior.

For advanced updates:

  • patch preserves omitted traits; put a field name in unsetTraits to remove a value owned by this source
  • snapshot replaces the full source-owned trait snapshot and requires unsetTraits to be empty
  • use unsetTraits instead of sending an empty string
  • state: "inactive" stops this source alias from creating new personalized candidates; it does not delete the shared CopyLoop contact
  • an already-bound user reporting a different email is quarantined for reconciliation instead of being silently moved or merged

Optional email.verification is source-asserted evidence:

{
"status": "verified",
"verifiedAt": "2026-07-27T11:59:00.000Z",
"method": "customer_account"
}

verifiedAt is required for verified and must not be later than observedAt. The unverified and unknown statuses must not include it. Omission preserves existing evidence only when the normalized address is unchanged; verification never carries to another address.

A successful write returns 202 Accepted after durable admission:

{
"receiptId": "rcpt_0198ec8e-3210-7abc-9123-0123456789ab",
"admissionDisposition": "new",
"requestId": "req_0198ec8e-3210-7abc-9123-0123456789ab",
"acceptedAt": "2026-07-27T12:00:00.100Z"
}

Store receiptId. 202 means CopyLoop durably accepted the operation; it does not mean processing has finished.

Read status with the same API key:

Terminal window
curl --fail-with-body \
--header "Authorization: Bearer $COPYLOOP_APPLICATION_SOURCE_KEY" \
"$COPYLOOP_API_BASE_URL/v1/receipts/$RECEIPT_ID"

Treat the response’s terminal boolean as authoritative. Known statuses are accepted, processing, applied, unresolved, quarantined, and failed.

  • unresolved is nonterminal because a later identify may resolve the signal; still use a bounded polling deadline
  • quarantined is terminal; correct the source data or arrange audited reconciliation instead of polling forever

These helpers cover all three write operations:

POST /v1/contacts/identify
POST /v1/events
POST /v1/consent
Terminal window
curl --fail-with-body \
--request POST "$COPYLOOP_API_BASE_URL/v1/contacts/identify" \
--header "Authorization: Bearer $COPYLOOP_APPLICATION_SOURCE_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: identify:obs_user_123_revision_42" \
--data @copyloop-identify.json

Save your application’s primary action first. Then enqueue the CopyLoop operation in a durable job queue or transactional outbox. A CopyLoop outage should not block signup, checkout, profile updates, or consent changes in your application.

Persist the exact body, idempotency key, receipt ID, and CopyLoop request ID. Redact credentials, email addresses, and request bodies from ordinary logs.

Send immutable activity to POST /v1/events. The initial catalog accepts trial.started schema version 1:

{
"eventId": "evt_456",
"subject": {
"type": "user",
"id": "user_123"
},
"event": "trial.started",
"schemaVersion": 1,
"occurredAt": "2026-07-27T12:05:00.000Z",
"properties": {
"plan": "pro",
"trialDays": 14
}
}

An event may arrive before identify. CopyLoop stores it as unresolved; it cannot personalize, enroll, send, or publish. A later identify for the same source user can resolve it as historical evidence.

Send consent decisions to POST /v1/consent. Consent is immutable evidence, not a contact trait. Include the email snapshot even when the user is already identified:

{
"decisionId": "consent_evt_789",
"subject": {
"type": "user",
"id": "user_123"
},
"scope": {
"type": "marketing_channel",
"channel": "email"
},
"contactPoint": {
"type": "email",
"address": "jane@example.com"
},
"decision": "withdraw",
"occurredAt": "2026-07-27T12:10:00.000Z",
"evidence": {
"actorType": "end_user",
"method": "preference_center",
"collectionPoint": "application_preferences",
"noticeVersion": "marketing-2026-07",
"contentRef": "consent-copy:marketing-2026-07",
"region": "EEA"
}
}

Accepted decisions are grant, deny, withdraw, and resubscribe. V1 accepts the marketing_channel and email scope.

An identify or event can never clear a denial or suppression. Production deny and withdraw install the negative source/address fence and CopyLoop email suppression in the same database transaction as the receipt. resubscribe requires supersedesDecisionId and is quarantined for policy review. grant does not by itself clear an existing suppression.

Retry network timeouts, connection resets, and HTTP 429, 500, 502, 503, and 504 with the same idempotency key and exact body. Use capped exponential backoff with jitter and honor Retry-After.

Do not retry 400, 401, 403, 409, 413, or 422 until the request or credential is corrected.

Developer API admission dispositions are:

  • new: first durable admission
  • transport_replay: same operation, idempotency key, and canonical body
  • domain_duplicate: a new transport key carried the same observation, event, or decision

Reusing an idempotency key or domain ID with different content returns 409.

  • Maximum request body: 65,536 bytes
  • Burst limit: 600 requests per minute, independently per credential and originating IP
  • Daily admission limit: configured per connection
  • Timestamp window: no more than 10 years old and no more than 10 minutes in the future

Negative consent bypasses the daily admission quota so quota exhaustion cannot discard a withdrawal, but it remains subject to abuse-protection burst limits.

Send only declared data needed for the integration. Do not send passwords, authentication tokens, API keys, payment-card data, national identifiers, private keys, health data, message bodies, or unrestricted free-form notes. CopyLoop does not put accepted raw properties in logs, workflow queue messages, analytics rows, or AI context.

Notify CopyLoop immediately about an access, export, correction, restriction, or erasure request that may include Application Source data. Deleting an ordinary contact does not represent completion of a legal rights request.

  1. Create a clearly named contact list with automations disabled.
  2. Create the Webhook or Developer API connection.
  3. For the Developer API, read GET /v1/contact-lists and configure the intended IDs.
  4. Send one controlled contact and store the returned receipt.
  5. Read the receipt until terminal is true and confirm status: "applied".
  6. Confirm the contact and intended list membership in CopyLoop.
  7. Verify an exact retry returns the same receipt and changed content under that key returns 409.
  8. For events, send one before identify and confirm it is initially unresolved.
  9. Identify the same source user and confirm resolution by email.
  10. Confirm no email was sent and no automation or Loop action gained authority from ingestion.

Monitor accepted, terminal, failed, unresolved, and quarantined receipts after launch. Reconcile a small sample against source records before expanding volume.