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 → 204 with Access-Control-Allow-Origin: *, methods POST, headers Authorization, Content-Type, Content-Encoding, max age a day) and every answer carries Access-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_batch
  • missing_level, invalid_level
  • missing_message, message_too_long
  • invalid_ts, ts_out_of_range
  • invalid_attrs, attrs_too_large, attrs_too_many_keys, attrs_too_deep
  • host_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.