Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 } }
FieldNotes
emailRequired. Lowercased and trimmed before it is stored.
first_name, last_nameOptional. Available in a template as {{first_name}}.
propertiesA JSON object of merge fields. Anything you put here is usable in a template and in a segment rule.
on_conflictupsert (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”.

  • skip returns what is stored, untouched. Use it for an import that must not overwrite edits somebody made on the dashboard.
  • error answers 409 already_exists. Use it when you believe the address is new and would rather hear that it is not.

Refusals

StatusnameWhen
422validation_erroremail is not an address, or properties is not a JSON object.
409already_existson_conflict: "error" and the address is on the list.
402quota_exceededThe plan’s contact ceiling. It is metered on contacts stored, so this is the write that crosses it.
403key_not_permittedA sending-access key. The audience needs a full-access one.