Ingestion
The write path. These three endpoints authenticate with an org-scoped Clerk API key (Authorization: Bearer <key>), sit behind the per-org rate limiter and the billing hard stop (429 over_quota), and dedupe retries via Idempotency-Key / messageId. All three name the pipe they arrive through: a sourceId of an api source, from which the server derives the app it belongs to and stamps both ids. An unknown id, or the id of a provider source such as Clerk, is a 422; an archived one is a 403. track also rejects an event name starting with system. with a 422, and a batch item carrying one comes back as that item’s own failed result: the prefix is reserved for the events Cowliss records itself (see Event patterns).
POST /v1/identify
Section titled “POST /v1/identify”Create or merge a profile
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "identifiers": { "anonymousId": "string" }, "traits": {}, "timestamp": "2026-01-15T09:30:00.000Z", "messageId": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 1–∞ |
data.identifiers |
record<anonymousId | userId | clerkId | email, string> | yes | |
data.traits |
object | no | default {} |
data.timestamp |
string (date-time) | no | |
data.messageId |
string | no |
Response 200
{ "data": { "id": "string", "orgId": "string", "environment": "development", "appId": "string", "sourceId": "string", "traits": { "key": {} }, "consent": { "key": true }, "mergedInto": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "identifiers": [ { "kind": "anonymousId", "value": "string", "createdAt": "2026-01-15T09:30:00.000Z" } ], "anonymous": true }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.environment |
“development” | “production” | yes | |
data.appId |
string | yes | |
data.sourceId |
string | yes | |
data.traits |
record<string, any> | yes | |
data.consent |
record<string, boolean> | yes | |
data.mergedInto |
string | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.identifiers |
object[] | yes | |
data.identifiers[] |
object | yes | |
data.identifiers[].kind |
“anonymousId” | “userId” | “clerkId” | “email” | yes | |
data.identifiers[].value |
string | yes | |
data.identifiers[].createdAt |
string (date-time) | yes | |
data.anonymous |
boolean | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
POST /v1/track
Section titled “POST /v1/track”Record one event
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "identifiers": { "anonymousId": "string" }, "event": "string", "properties": {}, "timestamp": "2026-01-15T09:30:00.000Z", "messageId": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 1–∞ |
data.identifiers |
record<anonymousId | userId | clerkId | email, string> | yes | |
data.event |
string | yes | length 1–200 |
data.properties |
object | no | default {} |
data.timestamp |
string (date-time) | no | |
data.messageId |
string | yes | length 1–∞ |
Response 200
{ "data": { "id": "string", "orgId": "string", "environment": "development", "appId": "string", "sourceId": "string", "profileId": "string", "event": "string", "properties": { "key": {} }, "timestamp": "2026-01-15T09:30:00.000Z", "receivedAt": "2026-01-15T09:30:00.000Z", "messageId": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | object | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
POST /v1/batch
Section titled “POST /v1/batch”Import one source’s calls in one request
Auth: Org API key (Authorization: Bearer <key>).
Request body
{ "data": { "sourceId": "string", "items": [ {} ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sourceId |
string | yes | length 1–∞ |
data.items |
any[] | yes | items 1–500 |
Response 200
{ "data": { "results": [ { "ok": true, "type": "identify", "id": "string" } ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.results |
object | object | object[] | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.