Capture API
Everything in Kilden enters through one endpoint:
POST https://ingest.kilden.io/captureContent-Type: application/jsonThe SDKs use it under the hood; you can call it directly from any backend, script or integration. There is no other write path.
Request
Section titled “Request”The body is always a batch — a single event is a batch of one:
{ "write_key": "YOUR_WRITE_KEY", "sent_at": "2026-07-11T12:00:00.000Z", "batch": [ { "uuid": "0197f9d2-5e5a-7cc3-b1a4-1f0e9a2b3c4d", "event": "order_completed", "distinct_id": "user_4821", "properties": { "total": 49.9, "currency": "EUR" }, "timestamp": "2026-07-11T11:59:58.000Z" } ]}| Field | Required | Notes |
|---|---|---|
write_key |
yes | Public or secret key, in the body (not a header). Secret keys mark events as server-side facts — see Trust levels |
sent_at |
no | When the client sent the request. Used only to correct client clock skew: real_time ≈ timestamp + (server_now − sent_at) |
batch |
yes | 1 to 1000 events |
identity_token |
no | Fallback slot for the identity verification JWT when a header is impossible (sendBeacon). The Authorization header wins if both are present |
Per event:
| Field | Required | Notes |
|---|---|---|
uuid |
yes | A valid UUID, generated by the client. Use UUID v7. This is the idempotency key: retries with the same uuid deduplicate, so retrying on any failure is always safe |
event |
yes | Event name, ≤ 200 chars. $-prefixed names are reserved for the system |
distinct_id |
yes | ≤ 512 chars. anon_-prefixed UUID v7 = anonymous; anything else is treated as an identified user id |
properties |
no | Any JSON object. Never schema-validated — the envelope is typed, the payload is yours |
timestamp |
no | ISO 8601. Defaults to server receive time; corrected with sent_at when both are present |
Headers:
Authorization: Bearer <jwt>— identity verification token for the whole batch (a batch belongs to one browser session). Max 4096 bytes.Content-Encoding: gzip— supported and recommended for large batches.
Limits: request body ≤ 5 MiB, batch ≤ 1000 events.
Response
Section titled “Response”200 as soon as the batch is validated and enqueued (capture never blocks on downstream processing):
{ "status": "ok" }Errors are plain text, not JSON:
| Status | Meaning |
|---|---|
400 |
Malformed request: invalid JSON body: …, invalid gzip body, write_key is required, batch must not be empty, batch exceeds 1000 events, batch[i]: uuid is not a valid UUID, batch[i]: event is required, batch[i]: event exceeds 200 chars, batch[i]: distinct_id is required, batch[i]: distinct_id exceeds 512 chars, batch[i]: properties is not valid JSON, identity token too large |
401 |
unknown write_key |
403 |
origin not allowed for this project — see allowed origins |
405 |
Method other than POST/OPTIONS |
Validation failures reject the whole batch — fix and retry (the UUIDs make that safe).
Server-side example
Section titled “Server-side example”curl -X POST https://ingest.kilden.io/capture \ -H 'Content-Type: application/json' \ -d '{ "write_key": "sk_your_secret_key", "sent_at": "2026-07-11T12:00:00Z", "batch": [{ "uuid": "0197f9d2-5e5a-7cc3-b1a4-1f0e9a2b3c4d", "event": "subscription_renewed", "distinct_id": "user_4821", "properties": {"plan": "pro", "mrr": 49}, "timestamp": "2026-07-11T12:00:00Z" }] }'Events sent with a secret key are marked source=server and verified=true — the secret key is the authentication. Never embed a secret key in a browser or mobile app; that’s what public keys are for.
/capture is a public write endpoint, CORS-open by design:
- Every response carries
Access-Control-Allow-Origin: *(no credentials). - Preflight
OPTIONSanswers204withAccess-Control-Allow-Methods: POST,Access-Control-Allow-Headers: Content-Type, Authorization, Content-Encoding,Access-Control-Max-Age: 86400.
Allowed origins
Section titled “Allowed origins”A public write key is visible in your page source, so anyone could copy it. To stop other sites from polluting your data, configure allowed origins in project settings: when the list is non-empty, browser requests whose Origin doesn’t match get 403.
- Patterns:
scheme://host[:port], with subdomain wildcards (https://*.example.com— doesn’t match the apex; list both if needed). - Empty list (default) = allow all.
- Requests without an
Originheader (curl, backends, mobile apps) always pass — that vector is covered by secret keys and identity verification, not by Origin matching. This is noise reduction against real-world key reuse in browsers, not a security boundary.