Skip to content

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.

FieldTypeDescription
objectstringAlways "contact".
idstringContact identifier — a UUID. Stable for the life of the contact, and the value you pass as a cursor when paginating.
emailstringThe contact's address, always lower-cased. Unique within your team.
first_namestring | nullnull if never set.
last_namestring | nullnull if never set.
unsubscribedbooleanThe global unsubscribe state. true means the contact is unsubscribed from all broadcasts and receives nothing at all, whatever their topics say.
unsubscribed_atstring | nullISO 8601 timestamp of when the contact last unsubscribed; null while subscribed.
bounced_atstring | nullISO 8601 timestamp of the contact's last hard bounce; null if they have never bounced.
sourcestringHow the contact got here — one of manual, csv, api, event, signup. Contacts created through this API are always api.
propertiesobjectThe contact's custom fields, resolved — see below. Always an object, {} when your team has no properties.
created_atstringISO 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 livesEffect
Global unsubscribeunsubscribed on this objecttrue and the contact receives nothing, from any broadcast.
Topic subscriptionContact topicsDecides 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.

Transactional email on your own domain.