Skip to content

Capture API

Everything in Kilden enters through one endpoint:

POST https://ingest.kilden.io/capture
Content-Type: application/json

The SDKs use it under the hood; you can call it directly from any backend, script or integration. There is no other write path.

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.

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).

Terminal window
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 OPTIONS answers 204 with Access-Control-Allow-Methods: POST, Access-Control-Allow-Headers: Content-Type, Authorization, Content-Encoding, Access-Control-Max-Age: 86400.

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 Origin header (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.