Skip to content

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.

  • A visitorId already produced by Sending events — identify only links an ID that already exists, it does not create one.
POST https://api.retidal.com/api/v1/identify

Authenticate with X-API-Key (also accepted as ?_ak= / ?key=).

visitorIdbodystringrequired

The anonymous visitor ID to associate. Alias visitor_id.

userIdbodystringrequired

The logged-in user ID. Alias user_id.

traitsbodyobject

Optional user attributes — see trait rules below.

Request
{
"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.

  1. Reserved keys become encrypted columns

    email, phone, name, locale, and timezone are extracted to encrypted top-level columns on the user record. Send an explicit null to clear one of these fields.

  2. Everything else is a custom trait

    Any other key (plan, company, …) is stored as JSON.

  3. 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’s dropped_keys array so you know what didn’t make it.

  4. Size and depth limits apply

    On POST /api/v1/identify, the serialized traits payload must be ≤ 8 KiB and nested ≤ 2 levels. Exceeding either returns 400.

Audit logs record only trait key counts, never trait values.

POST /api/v1/identify success returns 202:

json
{ "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.

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.

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.

JavaScript
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 login
await identify("vid_abc123", "uid_456", { email: "user@example.com", plan: "pro" });

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.

bash
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 filtered
  • POST /api/v1/identify itself rejected (400/401/500) — work through Events not arriving; this endpoint shares the ingestion-adjacent failure modes of POST /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.