Skip to content

Identity

Neither you nor your auth provider owns a profile’s id: Cowliss generates it (usr_…). What you send instead is who the write is about, as named identifiers, on every single call. That is what lets your app code and a Clerk source converge on one person without either side knowing the other’s ids.

identify and track take an identifiers map with at least one entry:

Terminal window
curl -X POST localhost:3400/v1/track \
-H "Authorization: Bearer $COW_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"sourceId": "src_01j2x8q7v9e3atn5m4kd7yz0bp",
"event": "viewed_pricing",
"identifiers": { "anonymousId": "anon_9f3…" },
"messageId": "msg-…"
}
}'

The kinds are a fixed enum, grown by code:

Kind What it is
anonymousId A pre-signup handle you mint (cookie, device id).
userId Your own id for the user.
clerkId The Clerk user id; this is what a Clerk source sends.
email Match by address: you are opting into “this mailbox is this person”. Addresses are lowercased before matching, so [email protected] and [email protected] are one identifier.

A call with zero identifiers is a 422: nothing could ever reference that profile again. A profile whose identifiers are all anonymousId is labelled anonymous in the dashboard.

Before any write, Cowliss looks up every identifier in the call:

  • None known: create a profile, claim them all. Anonymous-only traffic therefore accumulates on one anonymous profile per anonymousId.
  • Exactly one profile known: claim the new identifiers for it and write. This is how linking happens: there is no alias call. When your visitor signs up, one identify carrying { "userId": "user_1", "anonymousId": "anon_9f3…" } claims the userId onto the profile the anonymous history already belongs to, and the whole history is identified from then on.
  • More than one profile known: the profiles merge. The call names two different people’s known identifiers, so Cowliss merges the profiles into one, then writes.

Linking never rewrites or replays history: ClickHouse rows are immutable, and claiming an identifier does not fire journeys at past events. The live funnel reacts to live events; the linked history informs segments, timelines, and exports.

When a write’s identifiers resolve to more than one profile, Cowliss merges them automatically instead of refusing the write. The oldest profile, by createdAt, survives: the others get mergedInto set, and their identifiers move to the survivor. The survivor keeps its own traits, gains any key it lacked from the merged profiles (oldest merged first), then the incoming write applies on top. Consent is granted after the merge only where every merged profile had granted it, so an unsubscribe on either side survives. Events are never rewritten, the survivor’s reads union its merged ids, the same read-time resolution the alias map had. Memberships of the merged profiles are dropped, and the survivor is recomputed. Running journey instances of a merged profile keep running: their activities resolve the profile through mergedInto, so sends and trait writes land on the survivor. A system.profiles_merged system event lands on the survivor’s timeline, carrying the merged profile ids.

Merging never retro-triggers journeys: a merge changes reads and the one write that caused it, nothing else. There is no un-merge in v1. A request naming a merged-away profile id gets a 307 redirect to /v1/users/:survivorId.

Identifiers are unique per (orgId, environment, kind, value): the same clerkId in development and production is two profiles. Cross-environment identity never mixes, which is what makes a development app safe to point at real flows. See Environments.

DELETE /v1/users/:profileId removes the profile, its identifier rows, traits, suppression-mirror rows, quarantine entries, and, via ClickHouse lightweight deletes, every event in that environment, plus terminating in-flight journey instances. Erasing a survivor also removes every merged-away profile id and every identifier row that ever pointed at them, so a merge never leaves an erased person’s data behind under another id. See the users reference for erasure and export.