Skip to content

Tracking code

Everything in Cowliss starts with a write from your own application. There are two calls, identify and track, and one batch endpoint for history. This page is the whole tracking surface; the Quickstart is the four-minute version.

Every call carries the same two things:

  • identifiers: a map naming who the write is about. Cowliss generates the profile id and resolves your map to it, so you never invent one and never send one back. { userId: "user_1" }, { email: "[email protected]" }, { anonymousId, userId } together.
  • sourceId: the pipe the write arrives through, a src_ id belonging to one of your apps. The server looks up the source, derives the app, and stamps both ids on what it stores.

identify writes traits, track writes an event. Both create the profile if it is not there yet, so there is no separate registration step and no ordering requirement between them.

A product gives you two ids and they are not interchangeable:

Id Looks like You use it for
Source id src_01j2x8q7… Every write. It goes in your SDK config and on every HTTP body.
App id app_my-app Segment definitions, journey triggers, filtering the event feed.

You send the source; you filter on the app. An app can have several sources (its own api source, plus a provider such as Clerk), and everything they send lands on the same app and therefore the same profiles. See Apps and sources.

Terminal window
npm install @cowliss/sdk
import { Cow } from "@cowliss/sdk";
const cow = new Cow({
apiKey: process.env.COW_KEY!,
sourceId: "src_01j2x8q7v9e3atn5m4kd7yz0bp",
baseUrl: process.env.COW_API_URL,
});
Option Default Meaning
apiKey required Your org’s ingestion key, the Bearer credential on every request. One key serves every app.
sourceId none The source calls arrive through when they do not name one. Set it once per app.
baseUrl local dev The origin your Cowliss API is served at, with no trailing slash.
maxRetries 2 Retries after the first attempt, on network errors and 5xx only.
fetch global An injectable fetch, for tests and runtimes that do not have one.

A process that writes into two apps names sourceId on the call instead, or holds one client per app.

await cow.identify({
identifiers: { userId: "user_1" },
traits: { email: "[email protected]", plan: "pro" },
});

Traits merge: keys you send are written, keys you leave out are untouched, and a null value deletes the key. Segments recompute on the write, so the profile’s membership is correct the moment the call returns.

await cow.track({
identifiers: { userId: "user_1" },
event: "checkout_started",
properties: { cartId: "c_9", total: 4200 },
});
Field Required Notes
identifiers yes Who the event is about.
event yes Up to 200 characters. It may not start with system., which is reserved for the events Cowliss records itself.
properties no Any JSON object. Empty when omitted.
timestamp no ISO 8601 UTC, for a historical write. It may not be in the future. Defaults to now.
messageId yes on the wire The dedupe handle. The SDK generates one when you omit it.

Naming is yours to choose, and worth choosing once: event names are the vocabulary your segments and journey triggers are written against. checkout_started and purchase_completed read better in a trigger than evt_3.

A 4xx surfaces as a typed CowError with the API’s error code and is never retried. Network errors and 5xx retry with exponential backoff.

import { CowError } from "@cowliss/sdk";
try {
await cow.track({ identifiers: { userId: "user_1" }, event: "checkout_started" });
} catch (error) {
if (error instanceof CowError && error.code === "over_quota") {
// out of credit: the write did not land
}
}

An unknown sourceId, an archived source, a reserved event name, and a timestamp in the future are all rejected rather than quietly dropped. The codes and their statuses are in API conventions.

The SDK is a thin client over two endpoints, so any language works. Requests and responses use the { data } envelope.

Terminal window
curl -X POST $COW_API_URL/v1/identify \
-H "Authorization: Bearer $COW_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"sourceId": "src_01j2x8q7v9e3atn5m4kd7yz0bp",
"identifiers": { "userId": "user_1" },
"traits": { "email": "[email protected]", "plan": "pro" }
}
}'
Terminal window
curl -X POST $COW_API_URL/v1/track \
-H "Authorization: Bearer $COW_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg-checkout-1" \
-d '{
"data": {
"sourceId": "src_01j2x8q7v9e3atn5m4kd7yz0bp",
"identifiers": { "userId": "user_1" },
"event": "checkout_started",
"properties": { "cartId": "c_9" },
"messageId": "msg-checkout-1"
}
}'

A raw client must send messageId itself. The full request and response schemas are on the generated Ingestion page.

messageId is the dedupe key, and it doubles as the Idempotency-Key header. Send the same one twice and the event is stored once, which is what makes a retry after a lost response safe rather than a double write. Use a value derived from the thing that happened (an order id, a request id), not a fresh random per attempt, and a retry loop of your own dedupes the same way the SDK’s does.

One track call, synchronously, before the response: the profile is resolved or created, traits merge, segments recompute, and any journey whose trigger matches starts an execution. There is no queue to wait on and no eventual consistency to design around, so a read straight after a write sees it.

Two consequences worth knowing early:

  • Sending two identifiers in one call links them. There is no alias call. See Identity.
  • A write into an app in the development environment stays there: separate profiles, separate events, separate journey executions. See Environments.

Backfill goes through /v1/batch, which takes many items in one request, accepts historical timestamps, and writes quietly so a year of old orders does not fire a year of journeys at people. See Backfill and batch.

  • Apps and sources: a second app, and connecting Clerk so your auth provider’s users flow in with no integration code.
  • Identity: what happens when one person arrives under two identifiers.
  • Segments: turning the traits and events you just sent into audiences.