Contacts
The people you mail: find them across every audience, add them to a list, update their names, attributes and subscription, tag them, and delete them.
A contact is a person, stored once per workspace and keyed on their email address. The same contact can be on any number of audiences, and audience_ids lists every one. Adding an address that already exists joins that person to another list rather than creating a second copy.
Contacts carry three kinds of data: a first and last name, free-form tags (flat labels such as vip or churned), and attributes, which are values for the custom fields declared as contact properties. Segments filter on all three. For how contacts feed campaigns and sequences, see Campaigns and Automations.
The contact object
| Field | Type | Description |
|---|---|---|
id | string | The contact's id, a UUID. Every path that names a contact takes this, not the address. |
email | string | The address, trimmed and lower-cased. Unique within the workspace. |
first_name | string | null | First name, or null when there is none. |
last_name | string | null | Last name, or null when there is none. |
audience_ids | array of string | Every audience this person is on. Can be empty: removing someone from their last list does not delete them. |
tags | array of string | Tags, lower-cased with spaces turned into hyphens. Empty when there are none. |
attributes | object | Custom property values by key. Each value is a string, number or boolean. Empty object when there are none. Keys cannot contain . or a null character, or start with $. |
unsubscribed | boolean | Whether the contact is opted out of marketing. Campaigns and segments skip anyone with true. Set by an unsubscribe, and by importing someone with any status other than subscribed. |
unsubscribed_at | string (ISO 8601) | null | When the contact was last marked unsubscribed, or null if never or since resubscribed. |
last_opened_at | string (ISO 8601) | null | The last time this person opened a message, from open tracking. |
last_clicked_at | string (ISO 8601) | null | The last time this person clicked a link. |
last_sent_at | string (ISO 8601) | null | The last time a message to this address was accepted for delivery, or null when none has been recorded. Segments use it for exclude_unengaged_days. |
last_active_at | string (ISO 8601) | null | When the person was last active in your product, as you set it on create or update, or null when never set. Segments use it for engaged_within_days. |
created_at | string (ISO 8601) | When the contact was first created in the workspace. |
List contacts
GET /v1/contacts · requires contacts:read
Finds contacts across every audience in the workspace, so you can ask "is this address on any of our lists, and is it subscribed?" without knowing which list to look in. Results come newest first, by created_at and then by id for contacts created in the same instant (an import writes thousands at once).
To look up one address, pass email. The response is still a list: data holds the contact, or is empty when the address is not in the workspace. Take the id from it for every other contact call.
Page with limit and cursor as described in Pagination: pass next_cursor back as cursor while has_more is true. Filters apply to every page, so send the same q, tag and unsubscribed with each request. The cursor is the last row's creation time and id; treat it as opaque and URL-encode it. An email address as the cursor, the format this endpoint used to return, is still accepted and continues after that contact.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
email | string | none | An exact address. Trimmed and lower-cased before matching. Takes precedence over q when both are given. |
q | string | none | The start of an address, for a partial match: q=ana matches ana@example.com and anabel@example.com. Case-insensitive. Matches only from the beginning of the address, not anywhere inside it. |
unsubscribed | boolean | none | true for only opted-out contacts, false for only subscribed ones. Any other value is ignored. |
tag | string | none | Only contacts carrying this tag. Repeat it to require several: ?tag=vip&tag=beta returns people carrying both. Normalised like tags on write, so VIP matches vip. |
limit | integer | 50 | Page size, 1 to 100. See Pagination. |
cursor | string | none | The next_cursor from the previous page. An unreadable cursor is ignored and the first page is returned. |
Example
Response
200 OK
Create a contact
POST /v1/contacts · requires contacts:write
Adds one person to an audience. This is the same operation as adding a single contact to an audience, for a caller that starts from the person rather than the list. A contact always lands on a list, so audience_id is required. To add many people at once, send an array to POST /v1/audiences/{id}/contacts instead.
It is an upsert on the address, and safe to retry:
- A new address creates a contact on the audience.
- An address already in the workspace joins this audience and keeps its other memberships. The response is
201either way. Names are overwritten only when you send a non-empty value, tags you send are added to the ones the person already has (none are removed), and attributes are merged key by key. - Attributes are coerced to the type of the matching contact property, exactly as an update coerces them. A key with no declared property creates one with type
string, and the value is stored as a string:"seats": 5is saved as"5". Declarenumberandbooleanproperties before the first import, or a segment comparing against42ortruewill not match the stored strings. A key that is empty, contains.or a null character, or starts with$is refused with422before anything is written. statusis the person's standing with whoever mailed them before.subscribedis the default.unsubscribedsets the contact'sunsubscribedflag and writes a marketing-scoped suppression, so they still receive transactional mail such as receipts.bouncedandcomplainedset the flag and write a suppression for all mail. Any of the three also cancels the address's pending scheduled sends and ends its active automation enrolments, as an unsubscribe does.unsubscribed: trueis accepted as a synonym forstatus: "unsubscribed"; when both are sent,statuswins. The suppression's detail readsImported from the API.- An add never resubscribes anyone. Sending
status: "subscribed", or leaving it out, for a contact who is already unsubscribed leaves them unsubscribed and leaves their suppression in place. Resubscribing is a deliberate update. - Automations. When the address was not already on this audience, its
contact_addedautomations start for them, and each tag it did not already carry starts thetag_addedautomations watching that tag. Suppressed addresses are refused enrolment, so a contact added withstatus: "unsubscribed","bounced"or"complained"starts nothing. The response does not report how many started.
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
audience_id | string | yes | The audience's id from GET /v1/audiences, not its name. | |
email | string | yes | A valid email address. Stored trimmed and lower-cased. | |
first_name | string | no | Up to 120 characters. | |
last_name | string | no | Up to 120 characters. | |
tags | array of string | no | Up to 50 tags, each 1 to 60 characters. Lower-cased and trimmed, with runs of spaces turned into a hyphen. | |
attributes | object | no | Custom property values by key; each value a string, number or boolean. Keys cannot be empty, contain . or a null character, or start with $. | |
status | "subscribed" | "unsubscribed" | "bounced" | "complained" | no | "subscribed" | Standing with the previous sender. See above. |
unsubscribed | boolean | no | true is a synonym for status: "unsubscribed". Ignored when status is sent. | |
last_active_at | string (ISO 8601) | no | When the person was last active in your product, with a time zone. Kept only if later than the stored value. More than a day in the future is refused with 422 invalid_request. |
Example
Response
201 Created
A contact object, read back after the write, so an existing person shows every audience they are on and every tag and attribute they carry.
Errors
| Status | Type | When |
|---|---|---|
| 422 | invalid_request | audience_id is missing or empty. The message says a contact is always added to a list and points at GET /v1/audiences. |
| 422 | invalid_request | An attribute key is empty, contains . or a null character, or starts with $. The message starts attributes: and names the key, with details. |
| 422 | invalid_request | Any other field fails validation (message Validation failed, with details). |
| 422 | invalid_request | audience_id is not an audience in this workspace. The message starts audience_id:, and when the value matches an audience's name, it gives that audience's id. |
Retrieve a contact
GET /v1/contacts/{id} · requires contacts:read
Returns one contact. The path takes the contact's id. Passing the email address instead answers 404, and when that address belongs to a contact in the workspace the message gives you its id. To look someone up by address on purpose, use GET /v1/contacts?email=.
Path parameters
| Parameter | Description |
|---|---|
id | The contact's id. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No contact with that id in this workspace. |
Update a contact
PATCH /v1/contacts/{id} · requires contacts:write
Changes a contact's name, attributes or subscription. Fields you leave out are not touched.
- Names. A string replaces the name. There is no way to clear a name to
nullthrough the API; an empty string stores an empty name. - Attributes are merged: sending one key leaves the others as they were. Each value is coerced to the type of its declared contact property, the same way an add or import coerces it: for a
numberproperty a numeric string becomes a number,trueandfalse(or the strings"true","yes","false"and"no", in any case) become1and0, and anything else that is not a number becomes0; for abooleanpropertytrue,1, and the strings"true","1"and"yes"in any case becometrueand anything elsefalse;stringanddateproperties store the value as a string. A key with no declared property is stored exactly as sent and, unlike an import, does not create a property. A key that is empty, contains.or a null character, or starts with$is refused with422, and nothing in the request is written. unsubscribed: truedoes everything an unsubscribe does: sets the flag, writes a marketing-scoped suppression (detailunsubscribed via the API), cancels the address's pending scheduled sends and ends its active automation enrolments. Transactional mail still reaches them. When the flag is alreadytrueand a suppression that applies to marketing already exists (for example someone imported asbounced), the flag and suppression are left as they are, so a hard bounce or complaint is not rewritten as an unsubscribe, but pending sends are still cancelled and enrolments still ended.unsubscribed: falseresubscribes: it clears the flag and removes the marketing suppression. It is refused with409when the address has a hard bounce or any other suppression for all mail, or a complaint. Those have to be removed deliberately through suppressions first. The check runs before anything is written, so a refused request changes nothing, including the names and attributes in the same body.
The address, tags and audience memberships cannot be changed here, and any other field in the body is ignored. Change tags with Add or remove tags and memberships through audiences. Topic preferences are separate and are managed through topics.
Path parameters
| Parameter | Description |
|---|---|
id | The contact's id. |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
first_name | string | no | Up to 120 characters. | |
last_name | string | no | Up to 120 characters. | |
unsubscribed | boolean | no | true unsubscribes from marketing; false resubscribes. See above. | |
attributes | object | no | Custom property values to merge in, by key; each value a string, number or boolean. Keys cannot be empty, contain . or a null character, or start with $. | |
last_active_at | string (ISO 8601) | null | no | When the person was last active in your product, with a time zone. Kept only if later than the stored value, so a replayed or out-of-order update cannot move it back. More than a day in the future is refused. null clears it. |
Example
Response
200 OK
The updated contact object.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No contact with that id in this workspace. |
| 422 | invalid_request | An attribute key is empty, contains . or a null character, or starts with $. The message starts attributes: and names the key, with details. |
| 422 | invalid_request | Any other field fails validation (message Validation failed, with details). |
| 409 | invalid_state | unsubscribed: false for an address that complained or is suppressed for all mail. |
Delete a contact
DELETE /v1/contacts/{id} · requires contacts:write
Removes the person from the workspace, and with them every audience membership, tag, attribute and engagement date. To take someone off one list and keep them otherwise, use Remove a contact from an audience.
Their suppressions and topic preferences are kept on purpose. Those belong to the person, not the contact record, and discarding them would mean the next import mails someone who opted out. If the address is added again later it becomes a new contact with a new id, and its old suppressions still apply when mail is sent to it.
Deleting a contact does not unsubscribe the address, cancel its scheduled sends or end its automation enrolments. To stop mail to someone, update them with unsubscribed: true first, then delete them.
Path parameters
| Parameter | Description |
|---|---|
id | The contact's id. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No contact with that id in this workspace. |
List tags
GET /v1/contacts/tags · requires contacts:read
Every tag in use in the workspace, with how many contacts carry it, most used first and alphabetically among equal counts. Tags are free-form, so this is the only way to see what already exists. Check it before applying a new tag, so the workspace does not end up with both beta and beta-users.
At most 200 tags are returned, in the list envelope. The list is not paged: limit and cursor are ignored, next_cursor is always null, and has_more is true when 200 tags came back, meaning there may be less used tags this endpoint cannot return.
Example
Response
200 OK
Add or remove tags
POST /v1/contacts/{id}/tags · requires contacts:write
Adds and removes tags on one contact. A contact exists once per workspace, so the change applies wherever the person appears. Tags are trimmed, lower-cased and have runs of whitespace turned into a hyphen (then cut to 60 characters) before they are stored or matched, so VIP and vip are the same tag. Adding a tag the contact already has, or removing one they do not have, is not an error. add is applied before remove, so a tag in both ends up removed.
Any change re-checks the person's active automation enrolments. Adding a tag an automation lists in exit_tags, or removing one it lists in required_tags, ends that enrolment immediately, with cancel_reason exit_tag or required_tag_missing (see List enrolments). This is how you stop a trial sequence for someone who converted: add customer. See Automations.
A tag the contact did not already carry also starts every active automation with a tag_added trigger on that tag. Re-adding a tag the contact has starts nothing. A key that holds its sends for approval, or whose recipient allowlist leaves the address out, still adds the tag and starts nothing, because the steps go out later without the key.
Answers with the contact object after the change, the same object GET /v1/contacts/{id} returns. A body with neither array, or with both empty, changes nothing and returns the contact as it is.
Path parameters
| Parameter | Description |
|---|---|
id | The contact's id. |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
add | array of string | no | Tags to add. Up to 50, each at least 1 character. | |
remove | array of string | no | Tags to remove. Up to 50, each at least 1 character. |
Example
Response
200 OK
A contact object.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No contact with that id in this workspace. |
| 422 | invalid_request | add or remove is not an array of non-empty strings, or has more than 50 entries (message Validation failed, with details). |