Setup
1
Switch the environment to federated
organization:update.2
Read the identity secret
api_keys:write — this secret mints verified sessions, so reading it is
holding it, and a read gate would be the wrong gate. It is minted on first read:
until you have read it, no HMAC it could verify exists.3
Put it in your server's environment
Sign the claim
external_id and nothing else. No timestamp, no email, no
concatenation.
Boot
200 (or 201 when the contact was created)
The three hash outcomes
A wrong hash is refused outright and never downgraded to unverified. Silently
accepting a bad signature would make a typo in your HMAC code invisible — you would
ship it and find out much later.
What an unproven session cannot do
A hashless boot asserts anexternal_id and proves nothing, and it is a call
anybody who knows somebody’s external_id can make from a browser. So the
session it mints stops at the routes where that assertion would start to
matter, each with 403 unverified_session:
GET /v1/contact/me is not on the list: boot already answered with the
contact it resolved, so gating it would close nothing.
Anonymous sessions are unaffected. A visitor session is unverified too, but
there is nobody to impersonate — the row was created by the same boot call
now holding it, so its data is the caller’s own by construction. What the rule
is about is the one combination where the account on the other end belongs to
somebody else: unverified and identified.
The way past it is the way you would want in production anyway — configure the
identity secret and send the hash.
An unproven boot does not rewrite a profile
A hashless boot may create a contact, and it is always a sighting. What it may not do is change thename or email of a contact that already exists.
Those two are where security notices are addressed and where the JWT’s email
claim comes from. If an unproven assertion could overwrite them, whoever made
it would receive the victim’s own warnings. A contact the same call just
created has no previous owner to take anything from, so it keeps what it
arrived with.
The email is an attribute, always
This is the rule that keeps federated mode safe, and it is structural rather than a check. An email arriving through/v1/boot is stored on the contact row. It does not
resolve to an existing contact, and it does not become an identity edge.
The HMAC proves the external_id and only the external_id. If an email in the
same request could resolve to a contact, then a valid hash for your own id plus
someone else’s email would reach that someone’s data. That is the takeover shape,
and it is closed by construction.
To prove an email in federated mode, send a
magic link — the one flow that upgrades an
email from attribute to proven identity, and it works in both modes.
Anonymous boot
Boot without anexternal_id is a visitor sighting, and it works in both modes:
anonymous_id and external_id, the visitor is folded into the identified
contact and the attribution comes with it.
Send the UTMs from the page, where they actually are.
Rotating the secret
Failure modes
GET /v1/organization/environments/{id}/identity is re-readable by design: your
server needs the value in an environment variable, and a secret you can only see
once is a secret somebody pastes into a chat.