Skip to main content

Contacts

MethodPathScopeSDK
POST/api/v1/contactscontacts:writecontacts.upsert
GET/api/v1/contactscontacts:readcontacts.list / listPage / listAll
GET/api/v1/contacts/:idcontacts:readcontacts.get
POST/api/v1/contacts/:id/unsubscribecontacts:writecontacts.unsubscribe

POST /api/v1/contacts

Create or update contacts by email. Accepts a single object or an array of up to 1000. Idempotent on (project, email): an existing contact is updated and keeps its id.

FieldTypeNotes
emailstringRequired. Must be a valid address.
iduuidHonored on insert only — lets a migration preserve existing ids so already-issued unsubscribe links keep resolving.
statussubscribed | unsubscribed | pending | bouncedDefaults to subscribed on insert.
brandstringBrand slug; falls back to the project default.
sourcestringFree-form provenance label (signup-form, import).
tagsstring[]Replaces the existing tags.
consentobject{ source?, ip?, userAgent? } — compliance evidence for the opt-in.
curl -X POST https://mailroom.example.com/api/v1/contacts \
-H "Authorization: Bearer pk_live_…" \
-H "Content-Type: application/json" \
-d '[{"email":"ada@example.com","tags":["beta"],"source":"signup-form"},
{"email":"grace@example.com"}]'
{
"ok": true,
"contacts": [
{
"id": "9f0c1b7e-…",
"email": "ada@example.com",
"status": "subscribed",
"brand_id": "b1…",
"source": "signup-form",
"tags": ["beta"],
"created_at": "2026-07-28T10:12:03.221Z",
"updated_at": "2026-07-28T10:12:03.221Z"
}
]
}

422 invalid_request — bad email, empty array, or more than 1000 entries.

GET /api/v1/contacts

QueryTypeNotes
statusstringFilter by status.
tagstringContacts carrying this tag.
qstringEmail substring match.
limitnumberRows per page.
cursorstringOpaque cursor from a previous nextCursor.
curl "https://mailroom.example.com/api/v1/contacts?status=subscribed&limit=100" \
-H "Authorization: Bearer pk_live_…"
{ "ok": true, "contacts": [], "nextCursor": "eyJpZCI6…" }

nextCursor is null on the last page. Keep the other filters identical while paging. See Pagination.

GET /api/v1/contacts/:id

{ "ok": true, "contact": { "id": "9f0c…", "email": "ada@example.com",} }

404 not_found — no contact with that id in this project.

POST /api/v1/contacts/:id/unsubscribe

Programmatic opt-out. Same effect as the recipient clicking the unsubscribe link: status flips to unsubscribed and a contact.unsubscribed webhook fires.

{ "ok": true, "id": "9f0c…", "status": "unsubscribed" }

404 not_found — unknown contact id.

There is no delete endpoint; opting someone out is the terminal state.