Identity and aliases

Most people reach your product before they have an account. This page covers POST /api/v1/alias/, the endpoint that folds the id someone used before signing in into the id they use after, so the two halves of their history become one person.

Why identity matters

A visitor lands on your site or opens your app and does things — reads a page, starts a trial flow, taps around — long before you know who they are. The only id you have for them is one you made up: a random value in localStorage, a per-install identifier. Then they sign in, and suddenly you have a real one: user_42.

If you simply switch from one to the other, Tidings has no way to know they are the same person, and every read splits in half. The Users list shows two people. Retention counts them as two cohort members, one of whom never came back. A signup funnel never converts, because step one happened under the anonymous id and step two under the user id. The alias endpoint is how you tell us they are one person.

The alias endpoint

POST $TIDINGS_API/api/v1/alias/ — in production https://api.tidingshq.com/api/v1/alias/. It takes the same X-API-Key header as the events endpoint, so any client that can send events can call it. The key resolves the project, which is why the path carries no project of its own.

FieldTypeNotes
distinct_idstringThe canonical id — the one you want to keep. Usually your stable user id, user_42.
anonymous_idstringThe pre-sign-in id to fold in. It stops being a separate person.
terminalbash
$ curl -X POST $TIDINGS_API/api/v1/alias/ \
    -H 'X-API-Key: td_your_key_here' \
    -H 'Content-Type: application/json' \
    -d '{"distinct_id": "user_42", "anonymous_id": "anon_9f3c…"}'

{
  "distinct_id": "user_42",
  "anonymous_id": "anon_9f3c…",
  "created": true,
  "merged_events": 17
}

created is true only the first time — it says this call is what recorded the alias. merged_events is how many stored events this call moved. distinct_id comes back as the id everything now lives under, which can differ from the one you sent if that id had itself been aliased away earlier.

The call is idempotent. Repeating it returns the same shape with created: false and re-sweeps for any stray events that arrived under the anonymous id in the meantime, so it is cheap and safe to call on every launch or every sign-in. You do not need to remember whether you already merged this pair.

StatusMeaning
200Merged — or already merged, or a no-op. Body as above.
400A missing or empty id, the two ids equal to each other, or an id over 255 characters.
401Missing, unknown, or revoked API key.
409The anonymous id is already claimed. See when it returns 409.
429Rate limited. Alias shares the ingestion throttle with events/.

What a merge does

A merge does two things, and it is worth knowing both.

  • It rewrites the stored events that arrived under the anonymous id, so they now carry the canonical id.
  • It records the alias, which ingestion consults from then on: an event sent later under the anonymous id lands on the canonical id automatically. A client that is slow to switch ids, or that has a queue of events buffered from before sign-in, still ends up in the right place.

Because the rows themselves change, every read path is retroactively correct — the Users list, the events explorer, search, the schema view's unique-user counts, dashboard tiles, saved segments, paths, funnels, and retention all see one person with one history, with no special handling and no query cost.

The price of that is that a merge is irreversible: once the rows have been rewritten they no longer remember which id they arrived under. There is no unmerge. Alias when you are sure the two ids are the same person — which, at sign-in, you are.

Aliases are also depth-1: an id maps to exactly one canonical id, and a canonical id is never itself an alias. There are no chains to follow. If you alias into an id that has since been aliased away itself, the merge resolves to the id at the end of that hop and the response tells you which one it used.

Recommended flow

  1. Generate an anonymous id once and persist it — a random value in localStorage, a one-year cookie where consent allows, or a value stored with your app's install. A fresh id per visit makes Users and retention meaningless, because every visit looks like a new person.
  2. Send events under it while nobody is signed in: it is the distinct_id on every event.
  3. On sign-in, call alias once with the user's stable id as distinct_id and the stored anonymous id as anonymous_id. From then on send everything under the user id.
  4. On sign-out, generate a fresh anonymous id. Do not reuse the old one — it now belongs to that user, and the next person on the same browser or device would be merged into them.
browser or Nodejs
await fetch(`${TIDINGS_API}/api/v1/alias/`, {
  method: "POST",
  headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ distinct_id: user.id, anonymous_id: localStorage.getItem("anon_id") })
});

// then send every later event with distinct_id: user.id
Pythonpy
import requests

requests.post(
    f"{TIDINGS_API}/api/v1/alias/",
    headers={"X-API-Key": KEY},
    json={"distinct_id": user_id, "anonymous_id": anon_id},
)

When it returns 409

A 409 means the anonymous id you sent is already spoken for. There are two cases, and the response body says which:

  • It is already aliased to a different user. First claim wins: those events have already been rewritten to the first canonical id and cannot be handed back. In practice this is a shared browser or device where the anonymous id was reused after a sign-out.
  • It is itself a canonical id with aliases of its own. That would merge two identified people into one, which needs explicit intent rather than a client retry. (An id that merely has events is still mergeable — owning aliases is the evidence that it is someone in its own right.)

What to do: log it and carry on. A 409 is not a failure of the sign-in and it is not transient — the answer will be the same next time, so never retry in a loop. Discard the anonymous id, generate a fresh one for the next signed-out session, and keep sending events under the user id. The user's history from this point forward is correct; only the pre-sign-in tail is missing, and it belongs to whoever claimed it first.

One sequence that looks like a conflict but is not: calling alias(C, A) and then alias(A, C) — the same pair with the arguments swapped, which a confused retry can produce. That is a no-op 200, not a 409.