Create a contact
POST /contacts
The endpoint your signup handler calls. Send it the same address every time and it stays one contact — the address is the key, so you never store an id of ours.
{
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Ng",
"properties": { "plan": "pro", "seats": 9 }
}
| Field | Notes |
|---|---|
email | Required. Lowercased and trimmed before it is stored. |
first_name, last_name | Optional. Available in a template as {{first_name}}. |
properties | A JSON object of merge fields. Anything you put here is usable in a template and in a segment rule. |
on_conflict | upsert (default), skip or error when that address is already on the list. |
201 when the contact was created, 200 when it already existed — that is
how you tell the two apart without asking a second time.
{
"id": "8412",
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Ng",
"properties": { "plan": "pro", "seats": 9 },
"unsubscribed": false,
"created_at": 1735689600
}
The body says what the contact IS
A POST replaces properties — send {"plan": "pro"} for a contact who had
{"plan": "free", "seats": 3} and seats is gone. That is what makes removing a
field expressible at all.
When you only know that one field changed, use
PATCH, which merges.
on_conflict
upsert is the default because the caller is usually a signup or a sync, and
both mean “this is the contact now”.
skipreturns what is stored, untouched. Use it for an import that must not overwrite edits somebody made on the dashboard.erroranswers409 already_exists. Use it when you believe the address is new and would rather hear that it is not.
Refusals
| Status | name | When |
|---|---|---|
422 | validation_error | email is not an address, or properties is not a JSON object. |
409 | already_exists | on_conflict: "error" and the address is on the list. |
402 | quota_exceeded | The plan’s contact ceiling. It is metered on contacts stored, so this is the write that crosses it. |
403 | key_not_permitted | A sending-access key. The audience needs a full-access one. |