# Ingest API

The OTLP ingest endpoint: paths, authentication, content types, compression, request limits, status codes, and how to retry.

Every signal reaches Maple through one OTLP/HTTP gateway. Any OpenTelemetry SDK or Collector that can export OTLP over HTTP works without a Maple-specific exporter.

|              |                                                                                     |
| ------------ | ----------------------------------------------------------------------------------- |
| Base URL     | `https://ingest.maple.dev`, or your [region's](/docs/reference/regions) ingest host |
| Protocol     | OTLP over HTTP, `POST`                                                              |
| Auth         | `Authorization: Bearer maple_pk_…` (or `maple_sk_…`)                                |
| Encodings    | Protobuf (recommended) or JSON, optionally gzip                                     |
| Max body     | 20 MiB per request, measured on the compressed body                                 |
| Request time | 30 seconds per request                                                              |

## Endpoints

| Path                        | Payload                                                                            |
| --------------------------- | ---------------------------------------------------------------------------------- |
| `POST /v1/traces`           | OTLP `ExportTraceServiceRequest`                                                   |
| `POST /v1/logs`             | OTLP `ExportLogsServiceRequest`                                                    |
| `POST /v1/metrics`          | OTLP `ExportMetricsServiceRequest`                                                 |
| `POST /v1/events`           | [Product events](/docs/product-events/api)                                         |
| `POST /v1/sessionReplays/*` | Session replay chunks, sent by the [browser SDK](/docs/session-replay/browser-sdk) |
| `POST /v1/sessionEvents`    | Session timeline events, sent by the browser SDK                                   |

Use the ingest host of your organization's [region](/docs/reference/regions). A key from one region is rejected by the other. A standard exporter appends the signal path itself:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.maple.dev"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer maple_pk_…"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
```

## Authentication

Send your ingest key on every request, either as a Bearer token or in the `x-maple-ingest-key` header. The `Bearer` prefix is case-insensitive.

```http
Authorization: Bearer maple_pk_…
x-maple-ingest-key: maple_pk_…
```

Ingest keys can only send data, and each belongs to one organization. Use the **public** key (`maple_pk_…`) in browsers and mobile apps, where it ships to end users, and the **private** key (`maple_sk_…`) on servers. Find both under **Settings → Ingestion** in the dashboard. [Authentication](/docs/reference/authentication#ingest-keys) explains the difference.

The literal key `MAPLE_TEST` is accepted and returns `200`, but the data is discarded. Use it in CI or example code where you want the exporter to run without sending anything.

## Content types and compression

| `Content-Type`                                   | Read as       |
| ------------------------------------------------ | ------------- |
| `application/x-protobuf`, `application/protobuf` | OTLP protobuf |
| `application/octet-stream`                       | OTLP protobuf |
| anything containing `json` (`application/json`)  | OTLP JSON     |
| header missing                                   | OTLP protobuf |
| anything else                                    | `415`         |

For compression, send `Content-Encoding: gzip`. Omit the header (or send `identity`) for an uncompressed body. **gzip is the only compression supported.** `zstd`, `deflate` and `br` are rejected with `415`, so set your exporter's compression to `gzip` or `none`.

## Browsers

The gateway answers CORS preflights from any origin, so a browser can export directly. Allowed request headers are `Authorization`, `Content-Type`, `Content-Encoding`, `x-maple-ingest-key` and the `x-maple-*` headers the browser SDK sends. No response headers are exposed to scripts, so browser code cannot read `Retry-After`; back off on a fixed schedule instead.

## Status codes

Errors carry a JSON body with the same envelope as the [Maple API](/docs/reference/api#errors):

```json
{
	"error": {
		"_tag": "@maple/ingest/OrgQueueThrottled",
		"type": "rate_limit_error",
		"code": "ingest_queue_throttled",
		"title": "Ingest queue full for this org",
		"message": "This org's ingest queue is at capacity. No data was written; resend this batch after the suggested delay.",
		"retryable": true,
		"recovery": "retry",
		"retry_after_seconds": 1
	}
}
```

| Field                 | Meaning                                                                                                                      |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `_tag`                | The exact failure, stable across releases. Branch on this                                                                    |
| `type`                | The status family: `invalid_request_error`, `authentication_error`, `payment_error`, `rate_limit_error` or `api_error` (5xx) |
| `code`                | The short code in the table below                                                                                            |
| `title`, `message`    | Human-readable text                                                                                                          |
| `retryable`           | Whether resending the same batch can succeed                                                                                 |
| `recovery`            | `fix_request`, `reauthenticate`, `retry` or `contact_support`                                                                |
| `retry_after_seconds` | Present when a retry makes sense. The same value is in the `Retry-After` header                                              |

Checks run roughly in the order below, so a request fails on the first one it trips.

| Status | `code`                          | `_tag`                                 | Cause                                                                                                     | Retry?                           |
| ------ | ------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `200`  |                                 |                                        | Accepted and durably queued                                                                               |                                  |
| `401`  | `ingest_unauthorized`           | `@maple/ingest/Unauthorized`           | Missing, malformed or unknown ingest key, or a key sent to the other region's ingest                      | No. Fix the key                  |
| `429`  | `ingest_rate_limited`           | `@maple/ingest/RateLimited`            | More than 1,000 requests in flight for your organization                                                  | Yes, after 1 s                   |
| `413`  | `ingest_payload_too_large`      | `@maple/ingest/PayloadTooLarge`        | Body over 20 MiB                                                                                          | No. Send smaller batches         |
| `415`  | `ingest_unsupported_media_type` | `@maple/ingest/UnsupportedMediaType`   | Unknown `Content-Type` or `Content-Encoding`                                                              | No. Fix the exporter config      |
| `400`  | `ingest_bad_request`            | `@maple/ingest/BadRequest`             | Invalid gzip, a body that is not valid OTLP, or a malformed product event, session event or replay header | No                               |
| `400`  | `ingest_replay_body_not_gzip`   | `@maple/ingest/ReplayBodyNotGzip`      | A session replay chunk that is not a gzip stream                                                          | No                               |
| `402`  | `ingest_plan_limit_reached`     | `@maple/ingest/PlanLimitReached`       | No active subscription, or the plan's limit is reached                                                    | No. Check **Settings → Billing** |
| `429`  | `ingest_queue_throttled`        | `@maple/ingest/OrgQueueThrottled`      | Your organization's ingest queue is full. Nothing was written                                             | Yes, after 1 s                   |
| `429`  | `ingest_export_lane_full`       | `@maple/ingest/ExportLaneBackpressure` | The write path for your organization is backed up. Nothing was written                                    | Yes, after 2 s                   |
| `503`  | `ingest_unavailable`            | `@maple/ingest/ServiceUnavailable`     | Key lookup or another dependency is temporarily unavailable                                               | Yes, after 5 s                   |
| `503`  | `ingest_queue_unavailable`      | `@maple/ingest/QueueUnavailable`       | The batch could not be written to the durable queue. Nothing was written                                  | Yes, after 5 s                   |
| `503`  | `ingest_collector_unavailable`  | `@maple/ingest/CollectorUnavailable`   | An upstream collector failed or its response could not be read                                            | Yes, after 5 s                   |
| `503`  | `ingest_request_timeout`        | `@maple/ingest/RequestTimeout`         | The request took longer than 30 seconds                                                                   | Yes, after 5 s                   |
| `503`  | `ingest_encode_failed`          | `@maple/ingest/PayloadEncodeFailed`    | The batch could not be encoded for storage. Resending the same batch fails the same way                   | No. Contact support              |
| `500`  | `ingest_internal_error`         | `@maple/ingest/InternalError`          | Unexpected gateway error                                                                                  | No. Contact support              |

OpenTelemetry SDKs and the Collector already retry `429` and `503` with backoff, and drop on other `4xx` codes. That is the right behavior for every code above except `ingest_encode_failed`, which an exporter retries even though it cannot succeed.

## Batching

A request is limited by its compressed size (20 MiB), not its span or record count. The default batch sizes of the OpenTelemetry SDKs (512 spans or log records per export) and of the Collector's `batch` processor (8,192 items) stay far below that. If you raise them and a batch reaches the limit, the gateway answers `413` and the exporter drops the whole batch.

## Related

- [OpenTelemetry conventions](/docs/concepts/otel-conventions): the attributes Maple reads from what you send
- [Instrumentation guides](/docs/instrumentation): per-language exporter setup
- [Limits](/docs/reference/limits): query ranges, SQL and API rate limits
- [Regions](/docs/reference/regions): US and EU hosts
