Skip to main contentSkip to Content
Contacts

Contacts

Contacts are the people you interact with across all channels. Each contact can have custom attributes, multiple identities, consent preferences, tags, and a score.

List Contacts

GET /api/workspaces/{workspaceId}/contacts

Paginated list of contacts with optional filtering.

Auth: Session required

Query Parameters:

ParamTypeDefaultDescription
searchstring—Full-text search across name, email
tagIdstring—Filter by tag ID
segmentIdstring—Filter by segment membership
companyIdstring—Filter by company
localestring—Filter by locale
country, region, citystring—Filter by location (exact match); the dashboard sets these when you click a country or city on the map
anonymousstring—true or false
automationPausedstring—true or false
pagenumber1Page number
limitnumber20Items per page (max 100)
sortBystringcreatedAtcreatedAt, updatedAt, score or email
sortOrderstringdescasc or desc

Response:

{ "data": [ { "id": "ct_abc123", "email": "jane@example.com", "firstName": "Jane", "lastName": "Smith", "score": 42, "attributes": { "plan": "pro" }, "tags": [{ "id": "tag_1", "name": "VIP" }], "createdAt": "2026-01-15T10:00:00.000Z" } ], "pagination": { "page": 1, "limit": 50, "total": 1234, "totalPages": 25 } }

Create Contact

POST /api/workspaces/{workspaceId}/contacts

Auth: Session required

Request Body:

{ "email": "jane@example.com", "phone": "+1234567890", "firstName": "Jane", "lastName": "Smith", "externalId": "ext_123", "attributes": { "plan": "pro", "company": "Acme" }, "tags": ["VIP", "Enterprise"] }
FieldTypeRequiredDescription
emailstringNoContact email address
phonestringNoContact phone number
firstNamestringNoFirst name
lastNamestringNoLast name
externalIdstringNoYour system’s unique ID
locale, language, countrystringNoSee Language and country
region, city, timezonestringNoWhere the person is; timezone must be an IANA name such as Europe/Rome
latitude, longitudenumberNoSent together, or not at all
attributesobjectNoCustom key-value attributes. position is shown as the contact’s job title in the dashboard.
tagsstring[]NoTag names (created if new)

Response (201): Contact object with tags and consents. A contact.created event is logged on the contact’s timeline.

Get Contact

GET /api/workspaces/{workspaceId}/contacts/{contactId}

Returns the full contact profile including identities, consents, tags, recent events (50), recent messages (50), active segment memberships, and push subscriptions.

Auth: Session required

Update Contact

PATCH /api/workspaces/{workspaceId}/contacts/{contactId}

Partially updates a contact’s profile fields.

Auth: Session required

Request Body: Any subset of email, phone, firstName, lastName, externalId, locale, language, country, region, city, timezone, latitude, longitude, attributes, tags, ownerId, companyId, anonymous, automationPaused. Send null to clear a field; an attribute set to null is removed.

Every field that actually changed is written to the contact’s timeline as a contact.updated event whose metadata.changes lists { field, from, to } for each one (profile fields, attributes.<key> and tags). An update that changes nothing logs nothing. The dashboard’s Edit contact form uses this endpoint; the main fields are shown first and More fields reveals language, country, city, region, time zone, coordinates and external ID, followed by the workspace’s defined custom fields and free attributes.

Delete Contact

DELETE /api/workspaces/{workspaceId}/contacts/{contactId}

Permanently deletes a contact and all associated data.

Auth: Session required


Ingest (one call per event from your app)

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

Creates or updates a contact, records consent, joins static segments, starts the automations they trigger and logs an event, all from one request. This is the endpoint to call from your own product or from an automation tool such as n8n. It is described in full in Connect your app.


Import Contacts (Bulk)

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

Bulk import contacts with deduplication.

Auth: Session required

Request Body:

{ "contacts": [ { "email": "jane@example.com", "firstName": "Jane", "lastName": "Smith", "tags": ["imported"] }, { "email": "john@example.com", "firstName": "John" } ], "deduplicateBy": "email", "updateExisting": true }
FieldTypeRequiredDescription
contactsarrayYesArray of contact objects
deduplicateBystringYesField to deduplicate on (e.g., email)
updateExistingbooleanYesWhether to update existing contacts

Response:

{ "total": 100, "created": 85, "updated": 10, "skipped": 5, "errors": [ { "index": 23, "error": "Invalid email format" } ] }

Merge Contacts

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

Merges two contacts — the source contact’s data is transferred to the target, and the source is deleted.

Auth: Session required

Request Body:

{ "sourceContactId": "ct_source", "targetContactId": "ct_target" }

Transfers: events, messages, enrollments, segment memberships, inbox threads, identities, tags, and attributes.


Contact Timeline

GET /api/workspaces/{workspaceId}/contacts/{contactId}/timeline

Returns a unified reverse-chronological activity timeline.

Query Parameters:

ParamTypeDefaultDescription
limitnumber50Max 200
cursorstring—Pagination cursor

Response:

{ "timeline": [ { "id": "evt_123", "type": "page_view", "category": "event", "timestamp": "2026-01-15T10:00:00.000Z", "data": { "pageUrl": "/pricing" } }, { "id": "msg_456", "type": "SENT", "category": "message", "timestamp": "2026-01-15T09:30:00.000Z", "data": { "channel": "EMAIL", "subject": "Welcome!" } } ], "hasMore": true }

GET /api/workspaces/{workspaceId}/contacts/{contactId}/consent

Lists all consent records for a contact.

PUT /api/workspaces/{workspaceId}/contacts/{contactId}/consent

Creates or updates consent status for a specific channel.

Request Body:

{ "channel": "EMAIL", "status": "OPTED_IN", "source": "preference-center" }
ChannelValues
EMAILOPTED_IN, OPTED_OUT, PENDING, DOUBLE_OPT_IN_PENDING
PUSHSame as above
TELEGRAMSame as above
ONSITESame as above
SMSSame as above

Data Export (DSR)

GET /api/workspaces/{workspaceId}/contacts/{contactId}/export

“Export My Data” — Returns all data associated with a contact (identities, consents, tags, events, messages, segments, enrollments, push subscriptions, threads).

POST /api/workspaces/{workspaceId}/contacts/{contactId}/export

“Forget Me” — Anonymizes the contact by nulling PII, deleting identities and push subscriptions. Requires ADMIN role. Logs an audit event.

Response:

{ "success": true, "message": "Contact data has been anonymized" }

Language and country

A contact carries a locale (a BCP-47 tag such as en, en-CA or pt-BR) and a 2-letter country.

Any endpoint that creates or updates contacts accepts the pieces in whatever shape you have them, and stores them normalised (lowercase language, uppercase region):

You sendStored localeStored country
{ "locale": "en_ca" }en-CACA
{ "language": "EN", "country": "ca" }en-CACA
{ "locale": "pt-BR" }pt-BRBR
{ "country": "IT" }unchangedIT
{ "locale": "fr", "country": "BE" }fr-BEBE

This works on POST /contacts, PATCH /contacts/{id}, the import endpoint, form submissions (locale, language or country fields), the inbound webhook and the tracking script, which derives both from the browser language. A PATCH that sends only one part keeps the other.

Any real language is accepted, not a fixed list: the language must be an ISO 639 code and the region, when present, an ISO 3166 country (pt, pt-BR, sw-KE, zh-Hant-TW). A tag that is not one (xx, pt-ZZ, Portuguese) is rejected with 422 and a message saying which part is wrong. The same rule applies to a template’s default language and translations and to the workspace default language; the dashboard offers suggestions while you type but takes any valid tag.

Segment rules can filter on locale (the full tag), language (the tag without the region) and country.

Existing rows can be normalised with pnpm contacts:normalize-locales --apply.

A region the person chose with their language wins over the country they are in: a contact sent as { "locale": "pt-BR", "country": "MT" } keeps pt-BR and gets country MT. The country only completes a bare language, so { "locale": "es", "country": "MX" } reads Mexican Spanish. The sender applies the same rule when it picks a template language.

Location

Besides country, a contact has region, city, latitude, longitude and timezone. They are set by the ingest endpoint and the contact update endpoint, shown on the contact page, counted by GET …/contacts/geo and drawn on the contacts map. Coordinates are stored only as a pair; a value that cannot be used is dropped rather than failing the request.

Last updated on