Skip to main contentSkip to Content
Connect Your App

Connect your app

The simplest way to feed CampaignOS from your own product is one call per thing that happens to a user: a sign-up, a login, an opened checkout, a completed payment. The ingest endpoint takes that one call and does everything a contact needs: it creates or updates the person, records their consent, puts them in segments, starts the automations those segments trigger, and logs the event.

POST /api/workspaces/{workspaceId}/contacts/ingest

Auth: session, or an API key with contacts.write (and segments.write when segments is sent).

curl -X POST https://your-domain.com/api/workspaces/WORKSPACE_ID/contacts/ingest \ -H "Authorization: Bearer cos_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "type": "PAYMENT_OPEN", "email": "jane@example.com", "firstName": "Jane", "externalId": "user_8f2c", "locale": "pt-BR", "country": "BR", "city": "São Paulo", "region": "SP", "latitude": -23.55, "longitude": -46.63, "timezone": "America/Sao_Paulo", "emailConsent": { "status": "OPTED_IN", "source": "signup_email", "updatedAt": "2026-10-07T10:12:00.000Z" }, "segments": ["PAYMENT_OPEN"], "attributes": { "last_access_at": "2026-10-05T13:30:00.000Z", "total_paid": 0, "completed_payments": 0, "payment_url": "https://checkout.stripe.com/c/pay/cs_…" } }'
FieldTypeDescription
typestringThe event to record, for example SIGN_UP or PAYMENT_OPEN. Optional, but without it nothing is logged and no event-triggered automation starts.
emailstringFinds the contact; creates one when none exists.
contactIdstringFinds the contact by its CampaignOS id instead of the email.
externalIdstringYour own user id. Kept unique per workspace: an id already used by another contact is ignored.
firstName, lastName, phonestringProfile fields. Only the fields you send change.
locale, language, countrystringThe person’s language and country, in any mix; see Language and country.
region, city, latitude, longitude, timezonestring / numberWhere the person is. Coordinates are only stored as a pair. Anything unusable is dropped rather than rejecting the call.
attributesobjectMerged into the contact’s attributes: keys you send are set, keys you leave out are kept.
metadataobjectStored on the event only.
emailOptInbooleanRecords an email opt-in for a contact that has no email consent yet. Never overrides an opt-out.
emailOptOutbooleanOpts the contact out of email, as an unsubscribe link would, and fires the unsubscribe.email webhook. Wins over the other consent fields.
emailConsentobjectThe person’s own answer about marketing email, as your app holds it: status (OPTED_IN, OPTED_OUT or null), source, updatedAt. Applied unless the consent changed in CampaignOS after updatedAt, so an unsubscribe link wins over an older answer; without updatedAt it only fills a missing consent. A null status changes nothing, and an opt-out recorded this way does not fire unsubscribe.email. Wins over emailOptIn.
segmentsstring[]Static segments the contact joins, by name or id. Joining starts the automations that trigger on entering those segments. Unknown names are reported back, not created.

Response (200):

{ "contactId": "ct_abc123", "created": false, "segmentsJoined": ["PAYMENT_OPEN"], "unknownSegments": [], "eventId": "evt_9k2" }

What one call does, in order

  1. Contact — found by contactId, else by email; created when neither matches and an email was sent. Two calls for a brand-new address arriving at the same time end up on one contact.
  2. Consent — emailOptOut records an opt-out; otherwise emailConsent applies your app’s answer; otherwise emailOptIn records an opt-in if the contact has none. Automations only email contacts whose email consent is opted in; transactional campaigns ignore consent.
  3. Segments — the contact joins the listed static segments. A contact that is already in a segment does not “enter” it again, so automations triggered by entering the segment do not restart for them.
  4. Automations — entering a segment starts the automations that trigger on it (ten or fewer contacts run immediately, larger batches are queued).
  5. Event — the type is logged with its metadata. Automations triggered by that event start, dynamic segments are re-evaluated for the contact, and scoring rules run.

Steps 4 and 5 run after the response is sent, so the call returns in well under a second.

Choosing between a segment and an event

  • Use segments for states a person reaches once: signed up, finished the tutorial, paid. Each static segment keeps its members, so you can see and count who reached each state, and automations can skip people who already received a stage.
  • Use the event type for things that can happen again: a login, an opened checkout, a reset request. An automation with an Event trigger starts every time the event arrives (subject to its re-entry settings), which a segment cannot do.

Sending both is normal: "type": "PAYMENT_OPEN" with "segments": ["PAYMENT_OPEN"] records the state and lets an abandoned-checkout automation start on each attempt.

Attributes that automations rely on

Automations and segment rules can read any attribute (attributes.<name> in rules, {{contact.<name>}} in templates). A few conventions are worth keeping:

AttributeUsed for
last_access_atInactivity rules such as daysSince.attributes.last_access_at, the basis of win-back automations. Send it with a daily “user was active” event.
completed_payments, total_paid”Has paid” checks. Prefer completed counts over attempted ones.
payment_urlThe checkout a person left, so a reminder email can send them straight back to it.

Calling it from n8n, Make or Zapier

Point an HTTP request node at the endpoint with a header credential Authorization: Bearer <api key> and build the body from the incoming event. Keep the contract small: the endpoint tolerates missing fields, so map what you have and leave the rest out rather than sending empty strings.

  • POST /api/webhooks/inbound/{endpointId} — the signed inbound webhook, when you prefer per-endpoint secrets over API keys.
  • POST /api/workspaces/{workspaceId}/contacts/import — bulk import for existing lists.
  • POST /api/script/event — events from the browser through the website script.
Last updated on