Documentation

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

FieldTypeDescription
idstringThe contact's id, a UUID. Every path that names a contact takes this, not the address.
emailstringThe address, trimmed and lower-cased. Unique within the workspace.
first_namestring | nullFirst name, or null when there is none.
last_namestring | nullLast name, or null when there is none.
audience_idsarray of stringEvery audience this person is on. Can be empty: removing someone from their last list does not delete them.
tagsarray of stringTags, lower-cased with spaces turned into hyphens. Empty when there are none.
attributesobjectCustom 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 $.
unsubscribedbooleanWhether 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_atstring (ISO 8601) | nullWhen the contact was last marked unsubscribed, or null if never or since resubscribed.
last_opened_atstring (ISO 8601) | nullThe last time this person opened a message, from open tracking.
last_clicked_atstring (ISO 8601) | nullThe last time this person clicked a link.
last_sent_atstring (ISO 8601) | nullThe 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_atstring (ISO 8601) | nullWhen 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_atstring (ISO 8601)When the contact was first created in the workspace.
{
  "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
  "email": "ana@example.com",
  "first_name": "Ana",
  "last_name": "Lima",
  "audience_ids": ["b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e"],
  "tags": ["trial"],
  "attributes": { "plan": "pro", "seats": 5 },
  "unsubscribed": false,
  "unsubscribed_at": null,
  "last_opened_at": "2026-09-14T08:12:44.000Z",
  "last_clicked_at": null,
  "last_sent_at": null,
  "last_active_at": null,
  "created_at": "2026-09-01T10:30:00.000Z"
}

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

ParameterTypeDefaultDescription
emailstringnoneAn exact address. Trimmed and lower-cased before matching. Takes precedence over q when both are given.
qstringnoneThe 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.
unsubscribedbooleannonetrue for only opted-out contacts, false for only subscribed ones. Any other value is ignored.
tagstringnoneOnly 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.
limitinteger50Page size, 1 to 100. See Pagination.
cursorstringnoneThe next_cursor from the previous page. An unreadable cursor is ignored and the first page is returned.

Example

curl "https://api.sendraven.ai/v1/contacts?email=ana@example.com" \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "object": "list",
  "data": [
    {
      "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
      "email": "ana@example.com",
      "first_name": "Ana",
      "last_name": "Lima",
      "audience_ids": ["b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e"],
      "tags": ["trial"],
      "attributes": { "plan": "pro", "seats": 5 },
      "unsubscribed": false,
      "unsubscribed_at": null,
      "last_opened_at": "2026-09-14T08:12:44.000Z",
      "last_clicked_at": null,
      "last_sent_at": null,
      "last_active_at": null,
      "created_at": "2026-09-01T10:30:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

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 201 either 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": 5 is saved as "5". Declare number and boolean properties before the first import, or a segment comparing against 42 or true will not match the stored strings. A key that is empty, contains . or a null character, or starts with $ is refused with 422 before anything is written.
  • status is the person's standing with whoever mailed them before. subscribed is the default. unsubscribed sets the contact's unsubscribed flag and writes a marketing-scoped suppression, so they still receive transactional mail such as receipts. bounced and complained set 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: true is accepted as a synonym for status: "unsubscribed"; when both are sent, status wins. The suppression's detail reads Imported 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_added automations start for them, and each tag it did not already carry starts the tag_added automations watching that tag. Suppressed addresses are refused enrolment, so a contact added with status: "unsubscribed", "bounced" or "complained" starts nothing. The response does not report how many started.

Body

FieldTypeRequiredDefaultDescription
audience_idstringyesThe audience's id from GET /v1/audiences, not its name.
emailstringyesA valid email address. Stored trimmed and lower-cased.
first_namestringnoUp to 120 characters.
last_namestringnoUp to 120 characters.
tagsarray of stringnoUp to 50 tags, each 1 to 60 characters. Lower-cased and trimmed, with runs of spaces turned into a hyphen.
attributesobjectnoCustom 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.
unsubscribedbooleannotrue is a synonym for status: "unsubscribed". Ignored when status is sent.
last_active_atstring (ISO 8601)noWhen 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

curl -X POST https://api.sendraven.ai/v1/contacts \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "audience_id": "b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e",
    "email": "ana@example.com",
    "first_name": "Ana",
    "last_name": "Lima",
    "tags": ["trial"],
    "attributes": { "plan": "pro" }
  }'

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.

{
  "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
  "email": "ana@example.com",
  "first_name": "Ana",
  "last_name": "Lima",
  "audience_ids": ["b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e"],
  "tags": ["trial"],
  "attributes": { "plan": "pro" },
  "unsubscribed": false,
  "unsubscribed_at": null,
  "last_opened_at": null,
  "last_clicked_at": null,
  "last_sent_at": null,
  "last_active_at": null,
  "created_at": "2026-09-15T12:00:00.000Z"
}

Errors

StatusTypeWhen
422invalid_requestaudience_id is missing or empty. The message says a contact is always added to a list and points at GET /v1/audiences.
422invalid_requestAn attribute key is empty, contains . or a null character, or starts with $. The message starts attributes: and names the key, with details.
422invalid_requestAny other field fails validation (message Validation failed, with details).
422invalid_requestaudience_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

ParameterDescription
idThe contact's id.

Example

curl https://api.sendraven.ai/v1/contacts/5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70 \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

A contact object.

{
  "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
  "email": "ana@example.com",
  "first_name": "Ana",
  "last_name": "Lima",
  "audience_ids": ["b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e"],
  "tags": ["trial"],
  "attributes": { "plan": "pro", "seats": 5 },
  "unsubscribed": false,
  "unsubscribed_at": null,
  "last_opened_at": "2026-09-14T08:12:44.000Z",
  "last_clicked_at": null,
  "last_sent_at": null,
  "last_active_at": null,
  "created_at": "2026-09-01T10:30:00.000Z"
}

Errors

StatusTypeWhen
404not_foundNo 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 null through 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 number property a numeric string becomes a number, true and false (or the strings "true", "yes", "false" and "no", in any case) become 1 and 0, and anything else that is not a number becomes 0; for a boolean property true, 1, and the strings "true", "1" and "yes" in any case become true and anything else false; string and date properties 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 with 422, and nothing in the request is written.
  • unsubscribed: true does everything an unsubscribe does: sets the flag, writes a marketing-scoped suppression (detail unsubscribed 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 already true and a suppression that applies to marketing already exists (for example someone imported as bounced), 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: false resubscribes: it clears the flag and removes the marketing suppression. It is refused with 409 when 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

ParameterDescription
idThe contact's id.

Body

FieldTypeRequiredDefaultDescription
first_namestringnoUp to 120 characters.
last_namestringnoUp to 120 characters.
unsubscribedbooleannotrue unsubscribes from marketing; false resubscribes. See above.
attributesobjectnoCustom 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_atstring (ISO 8601) | nullnoWhen 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

curl -X PATCH https://api.sendraven.ai/v1/contacts/5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70 \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Ana Maria",
    "attributes": { "seats": 12 }
  }'

Response

200 OK

The updated contact object.

{
  "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
  "email": "ana@example.com",
  "first_name": "Ana Maria",
  "last_name": "Lima",
  "audience_ids": ["b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e"],
  "tags": ["trial"],
  "attributes": { "plan": "pro", "seats": 12 },
  "unsubscribed": false,
  "unsubscribed_at": null,
  "last_opened_at": "2026-09-14T08:12:44.000Z",
  "last_clicked_at": null,
  "last_sent_at": null,
  "last_active_at": null,
  "created_at": "2026-09-01T10:30:00.000Z"
}

Errors

StatusTypeWhen
404not_foundNo contact with that id in this workspace.
422invalid_requestAn attribute key is empty, contains . or a null character, or starts with $. The message starts attributes: and names the key, with details.
422invalid_requestAny other field fails validation (message Validation failed, with details).
409invalid_stateunsubscribed: 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

ParameterDescription
idThe contact's id.

Example

curl -X DELETE https://api.sendraven.ai/v1/contacts/5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70 \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
  "deleted": true,
  "note": "Suppression and topic preferences were kept — they belong to the person, not the list."
}

Errors

StatusTypeWhen
404not_foundNo 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

curl https://api.sendraven.ai/v1/contacts/tags \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY"

Response

200 OK

{
  "object": "list",
  "data": [
    { "tag": "trial", "count": 412 },
    { "tag": "customer", "count": 187 },
    { "tag": "vip", "count": 23 }
  ],
  "has_more": false,
  "next_cursor": null
}

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

ParameterDescription
idThe contact's id.

Body

FieldTypeRequiredDefaultDescription
addarray of stringnoTags to add. Up to 50, each at least 1 character.
removearray of stringnoTags to remove. Up to 50, each at least 1 character.

Example

curl -X POST https://api.sendraven.ai/v1/contacts/5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70/tags \
  -H "Authorization: Bearer $SENDRAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "add": ["Customer"],
    "remove": ["trial"]
  }'

Response

200 OK

A contact object.

{
  "id": "5f0c6a9e-2b7d-4c1e-9a3f-8d2e1b6c4a70",
  "email": "ana@example.com",
  "first_name": "Ana",
  "last_name": "Lima",
  "audience_ids": ["b7e2c1d4-6a3f-4e8b-9c5d-1f2a3b4c5d6e"],
  "tags": ["customer"],
  "attributes": { "plan": "pro", "seats": 5 },
  "unsubscribed": false,
  "unsubscribed_at": null,
  "last_opened_at": "2026-09-14T08:12:44.000Z",
  "last_clicked_at": null,
  "last_sent_at": null,
  "last_active_at": null,
  "created_at": "2026-09-01T10:30:00.000Z"
}

Errors

StatusTypeWhen
404not_foundNo contact with that id in this workspace.
422invalid_requestadd or remove is not an array of non-empty strings, or has more than 50 entries (message Validation failed, with details).