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

identify

await cow.identify({
identifiers: { userId: "user_1" },
traits: { email: "[email protected]", plan: "pro" },
});
Field Required Notes
identifiers yes Who the write is about. At least one; two in one call link them, within the app you sent through.
traits no Any JSON object. Merged, not replaced.
consent no Consent per purpose, keyed by path: marketing, transactional, or a name your repo declares, such as marketing.newsletter. See Consent.
timestamp no ISO 8601 UTC, for a historical write. Never in the future.
sourceId on the wire The client’s, unless the call names one.

identifiers is a map from kind to value: userId, anonymousId, email, clerkId. Cowliss owns the profile id (usr_…) and resolves your map to it, so you never invent one and never send one back.

// An anonymous visitor.
await cow.identify({ identifiers: { anonymousId: "anon_9f3" } });
// They sign up: naming both links them, history included. No alias call.
await cow.identify({
identifiers: { anonymousId: "anon_9f3", userId: "user_1" },
traits: { email: "[email protected]" },
});

Linking stops at the app. The sourceId on the call says which app the write belongs to, and a profile belongs to one app, so an identifier only ever joins identifiers under that app. Send the same userId to two of your products and you have two profiles, one per product, each with its own traits, consent and history.

Traits merge: keys you send are written, keys you leave out are untouched, and a null deletes the key. Writes are synchronous, so once the call has answered, segment membership is current and any automation it triggered has started. The full resolution rules, including what happens when two known people collide, are in Identity.

consent is a merge patch over the profile’s answers, keyed by purpose path. A key you leave out keeps its answer, and a name your app’s cow.json does not declare is a 422, with nothing written.

// The signup form's newsletter box, ticked.
await cow.identify({
identifiers: { userId: "user_1" },
consent: { "marketing.newsletter": true },
});

An unanswered purpose reads as its root decides: a marketing one is off, a transactional one is on. A marketing email reaches someone only when both its name and marketing are on, and granting a name turns its root on, so a signup that means yes to the newsletter sends one key. Turning marketing or transactional off turns off every name under it that your app declares, so a later grant of one name brings back that name and nothing else. Receipts and other mail sent as bare transactional are never asked. See Email templates.

Store a person’s language as the locale trait, a BCP 47 tag such as fr or fr-CA:

await cow.identify({
identifiers: { userId: "user_1" },
traits: { locale: "fr" },
});

It is a naming convention, not a setting: Cowliss neither checks nor reads it. Your automations read it to choose a template, as in Sending in several languages.

emailVerifiedAt and emailVerifiedAddress are Cowliss’s to write: sending them is a 422. See Email templates.