# Ingestion

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

<!-- GENERATED by apps/docs/scripts/generate-api-reference.ts. Do not edit. Regenerate with `pnpm -F cowliss-docs generate`. -->

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](/tracking/apps/)). 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](/automations/#the-system-namespace)).

## POST /v1/identify

Create or merge a profile

**Auth:** Org API key (`Authorization: Bearer <key>`).

**Request body**

```json
{
  "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**

```json
{
  "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](/api-conventions/#errors).

## POST /v1/track

Record one event

**Auth:** Org API key (`Authorization: Bearer <key>`).

**Request body**

```json
{
  "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**

```json
{
  "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](/api-conventions/#errors).

## 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**

```json
{
  "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**

```json
{
  "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](/api-conventions/#errors).

## GET /v1/whoami

Which app a source id writes into

**Auth:** Org API key (`Authorization: Bearer <key>`).

**Query parameters**

```json
{
  "sourceId": "string"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `sourceId` | string | yes | length 0–200 |

**Response 200**

```json
{
  "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](/api-conventions/#errors).
