Ingestion
The write path ingests traits and events using an organization API key (Authorization: Bearer <key>). All calls sit behind the organization rate limiter and the billing hard stop, and dedupe retries using Idempotency-Key or messageId. Every call requires the sourceId of an api source, from which Cowliss derives and stamps the app (see Apps tracking). Calls with an unknown or non-API sourceId are rejected, and an inactive one answers source_inactive. Event names starting with system. are rejected because that prefix is reserved for events Cowliss writes 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": {}, "consent": { "marketing.newsletter": true }, "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.consent |
record<string, boolean> | no | |
data.timestamp |
string (date-time) | no | |
data.messageId |
string | no |
Response 200
{ "data": { "id": "string", "orgId": "string", "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, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | 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 | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | 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", "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", "client": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.appId |
string | yes | |
data.sourceId |
string | yes | |
data.profileId |
string | yes | |
data.event |
string | yes | |
data.properties |
record<string, any> | yes | |
data.timestamp |
string (date-time) | yes | |
data.receivedAt |
string (date-time) | yes | |
data.messageId |
string | null | yes | |
data.client |
string | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | 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
Batch writes skip triggers on raw events, but segments still recompute and an automation triggered by segment entry can fire. A failing item is reported in the response and does not fail the rest of the batch.
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, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| 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 | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/whoami
Section titled “GET /v1/whoami”Which app a source id writes into
Auth: Org API key (Authorization: Bearer <key>).
Query parameters
{ "sourceId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
sourceId |
string | yes | length 0–200 |
Response 200
{ "data": { "orgId": "string", "app": { "id": "string", "name": "string" }, "sources": [ { "id": "string", "kind": "api", "name": "string", "configured": true, "lastReceivedAt": "2026-01-15T09:30:00.000Z" } ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.orgId |
string | yes | |
data.app |
object | yes | |
data.app.id |
string | yes | |
data.app.name |
string | yes | |
data.sources |
object[] | yes | |
data.sources[] |
object | yes | |
data.sources[].id |
string | yes | |
data.sources[].kind |
“api” | “clerk” | yes | |
data.sources[].name |
string | yes | |
data.sources[].configured |
boolean | yes | |
data.sources[].lastReceivedAt |
string (date-time) | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.