POST /v1/logs
Send one log entry, or a batch of them, as JSON.
Request
Headers:
Authorization: Bearer <key>: the app's API key. The app comes from the key, never from the payload.Content-Type: application/json.Content-Encoding: gzip(optional): send the body gzip-compressed. No other encoding is accepted.- Browsers: the endpoint answers a CORS preflight (
OPTIONS /v1/logs→204withAccess-Control-Allow-Origin: *, methodsPOST, headersAuthorization, Content-Type, Content-Encoding, max age a day) and every answer carriesAccess-Control-Allow-Origin: *, so a browser SDK can post with a key from any origin.
The body is one entry (a JSON object) or an array of entries: at most 1 MiB as sent and, when compressed, at most 1 MiB once decompressed; as an array, at most 1,000 entries.
{
"ts": "2026-11-05T12:00:00Z",
"level": "error",
"message": "payment failed",
"attrs": { "order_id": 8812, "err": "card_declined" },
"host": "web-3",
"env": "prod",
"trace_id": "4f1c9a"
}
Entry fields
| Field | Required | Description |
| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| level | yes | One of trace, debug, info, warn, error, fatal. |
| message | yes | Text, at most 8,192 bytes. |
| ts | no | RFC 3339 timestamp. Defaults to the time the server received the entry. Must be within the last 7 days and at most 1 hour ahead. |
| attrs | no | A JSON object, at most 65,536 bytes, 200 top-level keys and 64 levels of nesting (the object itself is level 1). |
| host | no | At most 128 bytes. |
| env | no | At most 128 bytes. |
| trace_id | no | At most 256 bytes. |
Field names are case-sensitive, duplicates are rejected, and an unknown field is an error.
attrs limits
| Limit | Value | Error |
| -------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------- |
| Size | 65,536 bytes | 400 attrs_too_large |
| Top-level keys | 200 | 400 attrs_too_many_keys |
| Nesting | 64 levels (the object itself is level 1) | 400 attrs_too_deep |
| Paths with a column of their own | 256 per storage part (max_dynamic_paths) | none: further paths are stored together, still searchable, but slower to search |
Responses
| Status | Body | Meaning |
| ------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 202 | {"accepted": n} | All n entries were validated and queued. Nothing is queued unless every entry is valid. |
| 400 | {"error": "…", "message": "…", "index": i} | Entry i (0-based) is invalid, or the body is not valid JSON. |
| 400 | invalid_encoding | The body is sent with Content-Encoding: gzip but is not valid gzip. |
| 401 | invalid_api_key | The key is missing, malformed, unknown or revoked. |
| 413 | body_too_large | The body is over 1 MiB, as sent or once decompressed. |
| 413 | too_many_entries | The batch has more than 1,000 entries. |
| 415 | unsupported_media_type | Content-Type is not application/json. |
| 415 | unsupported_encoding | Content-Encoding is anything but gzip. |
| 429 | rate_limited | The key sent more entries than its rate limit allows. Sent with Retry-After: <seconds>; nothing was queued. |
| 503 | backpressure or shutting_down | Ingest is overloaded or the server is shutting down. Sent with Retry-After: 1; retry. |
Validation error codes
A 400 carries one of these codes in error, and the index of the first invalid entry.
invalid_json,invalid_entry,unknown_field,empty_batchmissing_level,invalid_levelmissing_message,message_too_longinvalid_ts,ts_out_of_rangeinvalid_attrs,attrs_too_large,attrs_too_many_keys,attrs_too_deephost_too_long,env_too_long,trace_id_too_long
Rate limits
Each API key may send 5,000 entries per second on average, in bursts of up to 10,000; a batch counts as its number of entries and is accepted or refused whole. A self-hosted server sets its own limits, per key if needed (Self-hosting).
Retrying
On 429 and 503, wait the number of seconds in Retry-After, then send the same batch again, doubling the wait on each further refusal up to 30 seconds. Don't drop the batch: a refused request queued nothing, so resending it never stores an entry twice. Other 4xx answers fail the same way every time: fix the request instead.