Appearance
The contact object
A contact is one person in your audience: an address, a name, a subscription state, and whatever custom properties your team has defined.
Fields
The object returned by GET /v1/contacts/{id}, and by create, update and list.
| Field | Type | Description |
|---|---|---|
object | string | Always "contact". |
id | string | Contact identifier — a UUID. Stable for the life of the contact, and the value you pass as a cursor when paginating. |
email | string | The contact's address, always lower-cased. Unique within your team. |
first_name | string | null | null if never set. |
last_name | string | null | null if never set. |
unsubscribed | boolean | The global unsubscribe state. true means the contact is unsubscribed from all broadcasts and receives nothing at all, whatever their topics say. |
unsubscribed_at | string | null | ISO 8601 timestamp of when the contact last unsubscribed; null while subscribed. |
bounced_at | string | null | ISO 8601 timestamp of the contact's last hard bounce; null if they have never bounced. |
source | string | How the contact got here — one of manual, csv, api, event, signup. Contacts created through this API are always api. |
properties | object | The contact's custom fields, resolved — see below. Always an object, {} when your team has no properties. |
created_at | string | ISO 8601 timestamp of when the contact was created. |
A delete returns a stub rather than the object — { object, id, deleted: true }. See Delete a contact.
properties is the rendered view
properties is a flat map of property key → value, with each property's fallback already applied. It is not "the values this contact has"; it is "the values a template would render for this contact".
Two consequences worth knowing before they surprise you:
- Every property defined on your team appears, even ones this contact has no value for. A contact with nothing of their own still shows
"company_name": "Acme Corp"if that's the property's fallback. - A property with neither a value nor a fallback appears as
null. The key does not vanish.
json
"properties": {
"company_name": "Analytical Engines Ltd",
"plan": "Acme Corp",
"seats": 12,
"referrer": null
}So what you read back is what your recipients see: send {{{contact.company_name}}} in a template and the value in properties.company_name is exactly what lands in the inbox.
A number-typed property comes back as a JSON number; everything else as a string.
It is always a JSON object
properties encodes as {} when it's empty, never as []. Client code reading properties.plan should never have to defend against meeting an array.
The email address is the identity
One address, one contact, per team — creating a second contact with the same address is a 422. Addresses are normalised to lower case on save, which is what makes the address usable as a path parameter: every contact endpoint accepts the contact's id or its email address, so GET /v1/contacts/ada@example.com works without you looking the id up first.
Subscription state
Two independent things decide whether a contact receives a given broadcast:
| Where it lives | Effect | |
|---|---|---|
| Global unsubscribe | unsubscribed on this object | true and the contact receives nothing, from any broadcast. |
| Topic subscription | Contact topics | Decides one category only, and only while the contact is globally subscribed. |
bounced_at is a third signal: a contact who has hard-bounced is on your suppression list, and mail to them is recorded but not sent, whatever unsubscribed says.
Webhooks
contact.created, contact.updated and contact.deletedwebhook events fire when a contact is created, changed or removed — including through this API. If your own system is the one making those calls, remember that your handler will hear its own writes back.