> ## Documentation Index
> Fetch the complete documentation index at: https://docs.datalyr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest API

> Send one event over raw HTTP to POST ingest.datalyr.com/track: headers, body fields, limits, and every status code.

`POST https://ingest.datalyr.com/track` accepts one event from your server. Use it when
no Datalyr SDK fits your language or runtime.

```text theme={null}
https://ingest.datalyr.com/track
```

Ingest is a different host and a different credential from the
[`/v1` API](/api-reference/index). This endpoint writes. It reads nothing back.

## Before you start

You must have the write key from **Settings → API**. It starts with `dk_`, and it
selects the workspace on its own.

<Warning>
  Keep the write key on your server. Anyone holding it can write events into your
  workspace. Never ship it in a browser bundle or a mobile binary.
</Warning>

## Request

| Item   | Value                                     |
| ------ | ----------------------------------------- |
| Method | `POST`. Every other method returns `405`. |
| Path   | `/track`                                  |
| Body   | One JSON object. One event per request.   |

### Headers

| Header          | Required       | Value                                                                                             |
| --------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `X-API-Key`     | One of the two | Your write key, `dk_…`                                                                            |
| `Authorization` | One of the two | `Bearer dk_…`                                                                                     |
| `Content-Type`  | No             | `application/json`. Send it anyway, so a proxy does not rewrite the body.                         |
| `User-Agent`    | No             | Sets the transport `source` the event is stored under. See [Source detection](#source-detection). |

Send `X-API-Key` or `Authorization`, not both. `X-API-Key` wins when both arrive.

The workspace comes from the key. Do not put a workspace ID in the body, because
`/track` ignores it.

### Body fields

Every field below sits at the top level of the JSON object, except where the name starts
with `properties.`.

| Field                                                     | Type             | Required          | Behavior                                                                                                                  |
| --------------------------------------------------------- | ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `event` or `event_name`                                   | string           | Yes               | The event name. Missing returns `400`.                                                                                    |
| `distinct_id`, `user_id`, `anonymous_id`, or `visitor_id` | string           | Yes, at least one | Identity. See [Identity](/advanced/identity). Each also accepts its camelCase spelling.                                   |
| `event_id`                                                | string           | No                | Your own idempotency key. See [Duplicates](#duplicates). `eventId`, `messageId`, and `message_id` are accepted spellings. |
| `timestamp`                                               | string or number | No                | ISO 8601, epoch seconds, or epoch milliseconds. Defaults to receipt time.                                                 |
| `properties`                                              | object           | No                | Your event properties. Defaults to `{}`.                                                                                  |
| `context`                                                 | object           | No                | Client metadata. `library` and `version` get defaults.                                                                    |
| `context.session_id`                                      | string           | No                | Without it, Datalyr generates one that rolls at each UTC hour.                                                            |
| `context.userAgent`                                       | string           | No                | Overrides the `User-Agent` header for device detection.                                                                   |
| `external_ids`                                            | object           | No                | Extra platform IDs. An array is ignored, so send an object.                                                               |
| `ip`                                                      | string           | No                | The end user's IP. Send `"0"` to record no IP at all.                                                                     |

Two rules catch most integrations. A `timestamp` before `2020-01-01` falls back to
receipt time, and a `timestamp` in the future is clamped to now. Attribution belongs
inside `properties`, so a top-level `utm_source` is dropped.

### Attribution properties

These go in `properties`, and each one lands in its own column.

| Property                                                              | Holds                                                                         |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` | Campaign tagging                                                              |
| `gclid`, `fbclid`, `ttclid`, `gbraid`, `wbraid`, `oppref`             | Ad click IDs                                                                  |
| `clickId` with `clickIdType`                                          | One click ID, named by its type                                               |
| `lyr`                                                                 | The tracking-link tag. `install_referrer_url` is parsed for it as a fallback. |
| `value`, `revenue`, or `amount`                                       | Revenue. A numeric string is accepted.                                        |
| `currency`                                                            | ISO code. Datalyr uppercases it.                                              |
| `referrer`, `url`                                                     | Page context                                                                  |

`clickIdType` accepts `fbclid`, `gclid`, `ttclid`, `gbraid`, `wbraid`, `msclkid`,
`li_fat_id`, `epik`, `twclid`, `dclid`, `sclid`, `oppref`, `rdt_cid`, `obclid`, `irclid`,
`irclickid`, and `ko_click_id`. Datalyr drops any other type.

### Example

```bash theme={null}
curl "https://ingest.datalyr.com/track" \
  --header "X-API-Key: dk_YOUR_WRITE_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "event": "purchase",
    "user_id": "user_123",
    "anonymous_id": "anon_9f2c4e10",
    "event_id": "order_1001",
    "timestamp": "2026-08-20T12:00:00.000Z",
    "properties": { "value": 99.99, "currency": "USD", "gclid": "Cj0KCQ" }
  }'
```

## Batching

`/track` takes one event per request. There is no batch array on this endpoint, so
`{"events": [ ... ]}` posted here returns `400`.

The Web SDK batches on its own transport, which carries the public workspace ID rather
than a write key. Use the [Web SDK](/sdk-reference/web) for browser traffic, and one
`/track` request per event from a server.

## Duplicates

Send `event_id` on every request, and a retry cannot double-count.

| Behavior                 | Detail                                                                   |
| ------------------------ | ------------------------------------------------------------------------ |
| Scope                    | One workspace. The same ID in two workspaces is two events.              |
| Window                   | 7 days                                                                   |
| Response to a repeat     | `200` with `{"success": false, "error": "Duplicate event detected"}`     |
| After a `400` or a `500` | Retrying the same `event_id` is accepted, because the claim is released. |

<Note>
  Branch on the `success` field, not on the status code. A duplicate is a `200`.
</Note>

## Source detection

Datalyr reads the `User-Agent` header to record which transport the event arrived on.

| `User-Agent` contains                                                                                         | Stored `source` |
| ------------------------------------------------------------------------------------------------------------- | --------------- |
| `@datalyr/react-native`, `@datalyr/swift`, `datalyr-ios`, `datalyr-android`                                   | `mobile_app`    |
| `@datalyr/api`, `datalyr-node`, `datalyr-python`, `datalyr-ruby`, `datalyr-go`, `datalyr-java`, `datalyr-php` | `api`           |
| `mozilla`, `chrome`, `safari`, `firefox`, `edge`, `opera`                                                     | `browser`       |
| Anything else, or no header                                                                                   | `api`           |

Geo columns and device fingerprints are recorded for `mobile_app` events. An `api` event
carries the `ip` you send, and no geo.

## Responses

A `200` with `"success": true` means Datalyr accepted the event and queued it.

```json theme={null}
{ "success": true, "event": "purchase", "workspace": "ws_public_id" }
```

| Status | Body                                                                                          | Cause                                                                  | Fix                                                   |
| ------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------- |
| `200`  | `{"success":true,…}`                                                                          | Accepted.                                                              | None.                                                 |
| `200`  | `{"success":false,"error":"Duplicate event detected","event_id":"…"}`                         | This `event_id` arrived inside the last 7 days.                        | None. The first send counted.                         |
| `400`  | `{"error":"Request body is required"}`                                                        | The request carried no body.                                           | Send the JSON object.                                 |
| `400`  | `{"error":"Invalid JSON in request body"}`                                                    | The body did not parse to a JSON object.                               | Fix the JSON.                                         |
| `400`  | `{"error":"Event name is required (event or event_name field)"}`                              | No event name, or you posted an `events` array.                        | Send `event`. Send one event.                         |
| `400`  | `{"error":"Identity required: provide distinct_id, user_id, anonymous_id, or visitor_id"}`    | The body carried no identity field.                                    | Send at least one of the four.                        |
| `401`  | `{"error":"Missing API key. Please provide X-API-Key header or Authorization Bearer token."}` | No credential header.                                                  | Send `X-API-Key`.                                     |
| `401`  | `{"error":"Invalid API key"}`                                                                 | The key is unknown, over 256 characters, or the workspace is inactive. | Copy the write key again from **Settings → API**.     |
| `405`  | `{"error":"Method not allowed"}`                                                              | You sent `GET`, `PUT`, or `DELETE`.                                    | Use `POST`.                                           |
| `413`  | `{"error":"Payload too large. Maximum size is 256KB."}`                                       | The body passed 256,000 bytes.                                         | Trim `properties`. Split the work across events.      |
| `429`  | `{"error":"Rate limit exceeded. Maximum 1000 requests per second."}`                          | This key passed 1000 requests in one second.                           | Wait for `Retry-After`, which is `1`. Then retry.     |
| `500`  | `{"error":"Failed to enqueue event","details":"…"}`                                           | Datalyr could not queue the event.                                     | Retry with the same `event_id`.                       |
| `500`  | `{"error":"Internal server error","error_id":"…"}`                                            | A handler threw. The `X-Error-Id` header repeats the ID.               | Retry once. Send `error_id` to support if it repeats. |

## Limits

| Limit                             | Value          |
| --------------------------------- | -------------- |
| Request body                      | 256,000 bytes  |
| Requests per write key per second | 1000           |
| Events per request                | 1              |
| API key length                    | 256 characters |
| Duplicate window                  | 7 days         |

## CORS

`/track` answers `OPTIONS` with `204` and `Access-Control-Allow-Origin: *`. It allows
`Content-Type`, `X-API-Key`, and `Authorization`. It sets no
`Access-Control-Allow-Credentials`, so a browser request with `credentials: 'include'`
fails. This endpoint is built for server calls.

## Verify

1. Run the `curl` example above with your own write key.
2. Confirm the response reads `"success": true`.
3. Open **Events** in Datalyr.
4. Confirm a `purchase` row arrives within 30 seconds.

## When it doesn't work

| Symptom                             | Cause                                  | Fix                                                                |
| ----------------------------------- | -------------------------------------- | ------------------------------------------------------------------ |
| `401` on a key that works elsewhere | You sent the `dk_agent_` key.          | Send the `dk_` write key. Ingest takes no Agent key.               |
| `200` but no row in **Events**      | The response read `"success": false`.  | Read the `error` field. A duplicate `event_id` is the usual cause. |
| The event arrives with no campaign  | You put `utm_source` at the top level. | Move every UTM and click ID inside `properties`.                   |
| Two rows for one order              | You sent no `event_id`, then retried.  | Send a stable `event_id` on every request.                         |
| The event lands hours off           | Your `timestamp` did not parse.        | Send ISO 8601, epoch seconds, or epoch milliseconds.               |

## Next

* [Node.js SDK](/sdk-reference/node): the same contract, with retries handled for you.
* [Errors and limits](/api-reference/errors-and-limits): the `/v1` limits alongside these.
* [Agent access](/api-reference/agents): what an agent can read once events land.
