Skip to main content
In federated mode your product owns the account. UserKit needs to know which of your users is on the page, and it will not take the page’s word for it — your server signs the claim. The proof is an HMAC:
The identity_secret never reaches the browser. Compute the hash on your server, render it into the page, and hand it to boot. A secret in a bundle is a secret in every visitor’s hands — and this one mints verified sessions.

Setup

1

Switch the environment to federated

Requires organization:update.
2

Read the identity secret

Requires 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

Per environment. Your test deployment holds the test environment’s secret.

Sign the claim

The message is the 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 an external_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 the name 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 an external_id is a visitor sighting, and it works in both modes:
This is where first-touch attribution is born — on the anonymous contact, before this person has a name. When they later sign in and boot carries both 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

The previous secret keeps verifying for 24 hours — long enough for a fleet to redeploy, short enough that a stolen old secret has a deadline. Deploy the new value within the window; after it, hashes signed with the old secret are refused.

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.