Identifying users
Link an anonymous visitorId to a logged-in userId with POST /api/v1/identify, and understand what it does and does not unlock.
POST /api/v1/identify is a dedicated endpoint for associating an anonymous
visitorId with a logged-in userId, optionally attaching user traits. Call it
once a visitor logs in or completes registration.
This is a separate endpoint from event ingestion — it is not a Segment-style
POST /api/v1/t call with { "type": "identify" }. The ingest endpoint has no
type field at all.
Prerequisites
Section titled “Prerequisites”- A
visitorIdalready produced by Sending events — identify only links an ID that already exists, it does not create one.
Endpoint
Section titled “Endpoint”POST https://api.retidal.com/api/v1/identifyAuthenticate with X-API-Key (also accepted as ?_ak= / ?key=).
Request body
Section titled “Request body”visitorIdbodystringrequiredThe anonymous visitor ID to associate. Alias visitor_id.
userIdbodystringrequiredThe logged-in user ID. Alias user_id.
traitsbodyobjectOptional user attributes — see trait rules below.
{ "visitorId": "vid_abc123", "userId": "uid_456", "traits": { "email": "user@example.com", "phone": "+8613800138000", "name": "张三", "locale": "zh-CN", "timezone": "Asia/Shanghai", "plan": "pro" }}Both visitorId and userId are required on POST /api/v1/identify — omit either
one and you get 400.
Trait rules
Section titled “Trait rules”Reserved keys become encrypted columns
email,phone,name,locale, andtimezoneare extracted to encrypted top-level columns on the user record. Send an explicitnullto clear one of these fields.Everything else is a custom trait
Any other key (
plan,company, …) is stored as JSON.Dangerous keys are dropped, not stored
Keys whose name contains a dangerous substring —
password,pwd,idcard,passport,ssn,bankcard,cvv,token,secret,private_key,api_key, and similar — are silently dropped from storage and echoed back in the response’sdropped_keysarray so you know what didn’t make it.Size and depth limits apply
On
POST /api/v1/identify, the serializedtraitspayload must be ≤ 8 KiB and nested ≤ 2 levels. Exceeding either returns400.
Audit logs record only trait key counts, never trait values.
Response
Section titled “Response”POST /api/v1/identify success returns 202:
{ "ok": true, "dropped_keys": [] }dropped_keys lists any trait keys that were filtered out by the dangerous-key
check above — an empty array means every trait you sent was accepted.
Errors
Section titled “Errors”| Status | Meaning |
|---|---|
400 |
Invalid JSON, missing visitorId/userId, or traits failed the size/depth/shape check (POST /api/v1/identify). |
401 |
Missing or invalid API key (POST /api/v1/identify). |
500 |
Internal error while persisting the identity (no details leaked) — POST /api/v1/identify. |
What happens after identify
Section titled “What happens after identify”Calling identify has a side effect: Retidal backfills the userId onto every
event and visit this visitorId produced before the identify call. So pre-login
attribution — clicks, page views, cart activity — gets stitched to the identified
user retroactively, without you having to re-tag historical events.
Event-triggered email needs identify + email first
For a user to receive an event-triggered email, they must already be identified
with a non-empty traits.email. The trigger consumer looks up the user by
userId at send time — if there’s no identified user at all, it skips with
no_identified_user; if the user was identified but has no email on file, it
skips with no_email. Both cases fail silently from your side (only the
trigger log shows the skip reason). If you’re building a “send an invoice after
payment” flow, call identify (with traits.email) before you report the
paid event, not after.
Code sample
Section titled “Code sample”async function identify(visitorId, userId, traits) { const response = await fetch("https://api.retidal.com/api/v1/identify", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "your-api-key", }, body: JSON.stringify({ visitorId, userId, traits }), }); return response.json();}
// After a successful loginawait identify("vid_abc123", "uid_456", { email: "user@example.com", plan: "pro" });What this unlocks
Section titled “What this unlocks”Calling identify unlocks event-triggered email (which needs an
identified user with an email on file) and correct
cross-device attribution reporting, since both key off
userId rather than the anonymous visitorId alone.
Verify it worked
Section titled “Verify it worked”curl -s -X POST https://api.retidal.com/api/v1/identify \ -H "X-API-Key: $RETIDAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"visitorId":"vid_abc123","userId":"uid_456","traits":{"email":"user@example.com"}}'# expect: 202 { "ok": true, "dropped_keys": [] }# an empty dropped_keys array confirms every trait you sent was accepted, not filteredIf it doesn’t work
Section titled “If it doesn’t work”POST /api/v1/identifyitself rejected (400/401/500) — work through Events not arriving; this endpoint shares the ingestion-adjacent failure modes ofPOST /api/v1/t.- The call succeeds but the visitor/user link never seems to merge (pre-login activity isn’t showing up under the identified user) — work through Attribution looks wrong.
Next steps
Section titled “Next steps”Report the events these identified users produce.
See exactly what identity resolution guarantees — and what it doesn’t.