---
url: https://simpledatalogs.com/docs/api/ingest.md
description: >-
  The ingest endpoint, authentication, Content-Type and Content-Encoding, body
  shapes, browsers and CORS, and the other paths ingest serves.
---

# Sending data

Everything you send goes to one host, `edge.simpledatalogs.com`. This page covers the native endpoint, `/v1/ingest`, which takes events and log lines in JSON, NDJSON or plain text.

## The endpoint

```http
POST https://edge.simpledatalogs.com/v1/ingest
Authorization: Bearer <ingest key>
Content-Type: application/json | application/x-ndjson | text/plain
Content-Encoding: gzip                      (optional)
User-Agent: <your-sdk-or-app>/<version>
```

| Header | Required | Notes |
|---|---|---|
| `Authorization` | Yes | `Bearer sdl_pub_…` or `Bearer sdl_sec_…`. The key alone decides the environment and the deployment |
| `Content-Type` | Recommended | Decides how the body is read; see [Body shapes](#body-shapes). Without one, the body lands in Unparsed |
| `Content-Encoding` | No | Absent, `identity`, `gzip` or `x-gzip`. Anything else is answered `415` |
| `User-Agent` | Recommended | Name your SDK or app and its version, such as `my-game/1.4.0`. It is kept with every request |

A request is at most **512 KB as sent**. A gzip body counts its compressed bytes. Aim for 256 KB or less per request; see [Writing a sender](/api/sender-rules).

## Body shapes

Ingest stores the body exactly as sent and answers. The parser reads it afterwards, based on the `Content-Type`.

| Content-Type | Body |
|---|---|
| `application/json` | One event object, a JSON array of event objects, or a `batch` / `events` wrapper |
| `application/x-ndjson` | One event object per line |
| `text/plain` | Log lines in any common format, one entry per line, stack traces included |
| Anything else | Stored as is, and shown on the **Unparsed** tab with its reason |

The same two events, three ways:

::: code-group

```sh [JSON array]
curl -X POST https://edge.simpledatalogs.com/v1/ingest \
  -H "Authorization: Bearer sdl_sec_…" \
  -H "Content-Type: application/json" \
  -d '[
    {"event": "order_paid", "event_id": "0199f0c4-6c1e-7a3b-9d2e-5b8f1a2c3d4e", "ts": "2026-10-17T09:14:00Z", "props": {"amount": 29}},
    {"level": "error", "message": "Card declined", "event_id": "0199f0c4-6c1f-7c55-8e10-2a9b3c4d5e6f", "ts": "2026-10-17T09:14:02Z"}
  ]'
```

```sh [NDJSON]
curl -X POST https://edge.simpledatalogs.com/v1/ingest \
  -H "Authorization: Bearer sdl_sec_…" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary $'{"event":"order_paid","event_id":"0199f0c4-6c1e-7a3b-9d2e-5b8f1a2c3d4e","ts":"2026-10-17T09:14:00Z","props":{"amount":29}}\n{"level":"error","message":"Card declined","event_id":"0199f0c4-6c1f-7c55-8e10-2a9b3c4d5e6f","ts":"2026-10-17T09:14:02Z"}'
```

```sh [Plain text]
curl -X POST https://edge.simpledatalogs.com/v1/ingest \
  -H "Authorization: Bearer sdl_sec_…" \
  -H "Content-Type: text/plain" \
  --data-binary $'level=info event=order_paid amount=29\n2026-10-17 09:14:02 ERROR Card declined'
```

:::

The fields of each object are on [Events and log objects](/api/events), and the text formats on [Text log lines](/api/log-lines).

::: tip Prefer NDJSON for servers
NDJSON with `event_id` and `ts` is the most robust shape for batches: one bad line goes to Unparsed alone, and retries never count twice. Plain text lines carry no `event_id`, so a retried text request counts twice.
:::

## Compressing with gzip

Gzip is worth it for anything bigger than a few events. Set `Content-Encoding: gzip` and send the compressed bytes:

```sh
gzip -c events.ndjson | curl -X POST https://edge.simpledatalogs.com/v1/ingest \
  -H "Authorization: Bearer sdl_sec_…" \
  -H "Content-Type: application/x-ndjson" \
  -H "Content-Encoding: gzip" \
  --data-binary @-
```

The 512 KB limit applies to the compressed bytes. A body may inflate to at most 8 MB; a larger one, or a corrupt one, goes to Unparsed whole (`decompressed_too_large`, `invalid_gzip`).

`br`, `zstd` and `deflate` are refused with `415`.

## A successful answer

```http
HTTP/1.1 202 Accepted

{"accepted":"0199f0c4-7d20-7e8a-b1c2-d3e4f5a6b7c8"}
```

`202` is the only success. It is sent only once the body is durably stored. Anything else means it was not stored, so keep the data and retry; see [Responses](/api/responses).

## Browsers and CORS

Public keys are meant for websites, so ingest answers browsers on every path.

* Every answer, refusals included, carries `Access-Control-Allow-Origin: *` and exposes `Retry-After`.
* `OPTIONS` on any path is a preflight. It answers `204` without a key and allows the `Authorization`, `Content-Type` and `Content-Encoding` headers.
* There is no credentials mode: ingest never uses cookies.

```js
await fetch('https://edge.simpledatalogs.com/v1/ingest', {
  method: 'POST',
  headers: { Authorization: 'Bearer sdl_pub_…', 'Content-Type': 'application/json' },
  body: JSON.stringify({ event: 'page_view', event_id: crypto.randomUUID(), ts: new Date().toISOString() }),
})
```

::: warning Never put a secret key in a web page
Only `sdl_pub_…` keys belong in browsers and apps. The [JavaScript SDK](/sdks/javascript) handles batching, retries and page hide for you.
:::

## Other paths

Ingest also speaks the wire protocols of tools you may already use, so they switch by changing an endpoint and a key. Each path shares `/v1/ingest`'s rules: the key check, the rate limit, the 512 KB body limit, gzip, and success only once the body is stored.

| Path | For | Guide |
|---|---|---|
| `POST /v1/logs` | OpenTelemetry logs (OTLP/HTTP, protobuf or JSON) | [OpenTelemetry](/integrations/opentelemetry) |
| `POST /v1/batch`, `/v1/track`, `/v1/page`, `/v1/screen`, `/v1/identify`, `/v1/group`, `/v1/alias` | Segment HTTP Tracking API | [Segment](/integrations/segment) |
| `POST /api/<project>/envelope/`, `/api/<project>/store/` | Sentry SDKs | [Sentry](/integrations/sentry) |
| `POST /api/v2/logs` | Datadog logs intake | [Datadog logs](/integrations/datadog) |
| `POST /loki/api/v1/push`, `/api/prom/push`, `/otlp/v1/logs` | Loki push API | [Loki](/integrations/loki) |
| `POST /services/collector[/event][/1.0]`, `/services/collector/raw[/1.0]` | Splunk HEC | [Splunk HEC](/integrations/splunk) |
| `POST /vercel/v1/logs` | Vercel Log Drains, signed with the ingest key | [Vercel](/integrations/vercel) |
| `POST /heroku/v1/logs` | Heroku HTTPS log drains | [Heroku](/integrations/heroku) |
| `POST /v1/hooks/<token>` | Webhooks from Stripe, GitHub and other services; the URL token replaces the key | [Inbound webhooks](/integrations/webhooks) |
| `POST /openrouter/v1/traces` | OpenRouter Broadcast traces (OTLP/JSON), one `llm_call` event per LLM call | [OpenRouter](/integrations/openrouter) |
| `GET /up` | Health check, answers `200 ok` | — |

Other OpenTelemetry traces and metrics (`/v1/traces`, `/v1/metrics`) are not stored. They always answer `404` with a message naming the exporter setting to turn off.

## Next steps

* [Events and log objects](/api/events): every field and its limits.
* [Responses](/api/responses): every status code and what to do about it.
* [Writing a sender](/api/sender-rules): batching, the outbox and retries.
