Reference

Headers, errors & limits

The shared header, response-shape, and rate-limit contract across both server ingest surfaces.

Applies to both POST /api/v1/ingest (Surface 1a) and POST /api/v1/ingest/:slug (Surface 1b).

Request headers

HeaderSurfaceRequiredValue
Authorization1a onlyYesBearer ingt_srv_<token>
X-BGL-Timestamp1b onlyYesUnix seconds, within ±5 minutes of server time
X-BGL-Signature1b onlyYesHex HMAC-SHA256(secret, "{timestamp}.{rawBody}"), optional sha256= prefix
X-BGL-Idempotency-KeyBothNo1–255 char string, dedups retries for 24h
Content-TypeBothYesapplication/json

Response shapes

Both surfaces return exactly one of two shapes — there is no third status code:

StatusBodyWhen
202 Accepted{ "submission_id": "<uuid>" }The submission was accepted and scored.
200 OK{ "status": "rejected" }Anything went wrong — auth, rate limit, signature, form state, body validity.

Why rejections are opaque

A distinguishable response per failure mode (401 for a bad token, 429 for rate limiting, 400 for a bad signature) would let an attacker enumerate valid tokens or form slugs by watching which status code comes back. Every rejection reason — no matter how different internally — collapses to the same 200 { "status": "rejected" } on the wire. If you need to know why a specific submission was rejected, the real reason is logged and visible in Settings → Ingest tokens → Diagnostics, scoped to your own workspace.

Rate limits

Per workspace, tracked separately per surface (a burst on 1a doesn't count against 1b's bucket):

WindowLimit
Per minute1,000 submissions
Per day50,000 submissions

Exceeding either limit returns the same opaque 200 { "status": "rejected" } — there's no 429.

Body limits

Both surfaces cap the raw request body at 128 KB. Oversized bodies are rejected before any parsing happens.

Legacy endpoint sunset

The pre-v1 /api/ingest/:slug endpoint (optional signature, no replay protection) was deprecated on 2026-05-26 and permanently retired on 2026-06-25 — every request now returns 410 Gone. If you're still pointed at it, migrate to one of the two endpoints on this page; see the in-app migration notes for the full cutover checklist.