Identity edges
A contact is reached through identity edges — values that resolve to it. There are three kinds:
That difference is the whole model.
external_id and email are identifying: a value that already belongs to
another contact is a duplicate to merge by hand, never a silent re-point.
Silently moving an email between contacts is how one person ends up reading
another’s data.
anonymous_id is the opposite: a device cookie belongs to whoever signed in last,
so the last claimant takes it.
Identify
POST /v1/contacts is create-or-update. Send whatever your backend knows; at
least one of external_id, email or anonymous_id is required.
201 when the contact was created, 200 when it already existed — both with
created in the body so you do not have to read the status.
Resolution order is external_id, then email: your own primary key is the
stronger claim.
Name and email are attributes. A change is a change, not a new person — they are
updated on every call.
The email identity edge is only attached when it is free. A conflict leaves the
edge where it is and hands you a duplicate to resolve.
A call carrying only an
anonymous_id is a visitor sighting, not an
identification. identified stays false.Folding the visitor in
When ananonymous_id arrives alongside an identifying kind, the visitor contact
it belonged to is folded into the identified one — automatically, in the same
transaction.
That is the moment attribution earns its keep: the campaign that brought someone
to your landing page was recorded on the anonymous row, weeks before they had a
name, and it survives into the contact you now know.
Attribution
First touch, captured on the visitor at first load and preserved through every merge:/v1/boot (from the page, where the UTMs actually are) or on
/v1/contacts from your server.
Identified, and verified
Two different flags, and the difference matters.boolean
This contact is more than a visitor — an
external_id attached, or an email was
proven.boolean
The address was proven, by a link arriving in it: a verification link, a
password-reset link, or a magic link.
/v1/boot is
stored on the row and never becomes an identity edge, because the HMAC proves the
external_id and only the external_id. A valid hash for your own id plus
someone else’s email must never touch that someone’s contact.
There is a third verified, on the contact session,
which says the identity behind that session was proven. Related, not the same.
Merges
There is no un-merge. So merges happen on purpose.{id} in the URL is the source — the row
that disappears — and target_id in the body is the survivor. Requires
customers:write.
Every merge writes a record carrying who did it, when, and what moved:
400.
Automatic merges do happen — folding a visitor into the contact that just claimed
its anonymous_id — and they are recorded the same way, with a different kind.
Reading contacts
Two surfaces, two postures:limit (default 50, capped at 200) and offset. The cap is
deliberate: enough for a screen, small enough that nobody accidentally downloads
their whole audience.