Skip to content
Cowliss is in beta: features and pricing may change.

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 https://api.cowliss.com/v1/track \
-H "Authorization: Bearer $COW_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"sourceId": "'"$COW_SOURCE_ID"'",
"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. Mail goes to the email trait, and to the newest email identifier when the profile has no trait.

If a Clerk source feeds this app, signing up and logging in are already events (system.signed_up, system.logged_in), so do not send your own alongside them.

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. Every profile a call can resolve belongs to the same app as the call, so a merge never spans apps.

Linking never rewrites or replays history: events are immutable, and claiming an identifier does not fire automations 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 automation instances of a merged profile keep running: their activities resolve the profile through mergedInto, so sends 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 automations: 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, appId, kind, value): the same clerkId always resolves to the same profile, whichever of that app’s sources it arrives through. The app comes from the sourceId on the call, so your calls never name it.

A profile belongs to one app, and so identity stops at the app’s edge. The same email address signing up on two of your products is two profiles with two usr_ ids, and no call, no identifier kind and no merge ever joins them. If you want your own products to share an identity, join them at your own edge and send the id you chose as a userId to both.

Identity never crosses organizations either, so a second organization is where to send test traffic that would otherwise claim a real person’s identifiers. See Your repo.

DELETE /v1/users/:profileId removes the profile, its identifiers, traits, suppression state, and every one of its events, and stops any automation it is in. 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.