POST /v1/batch
Send 1 to 1,000 events in one request, authenticated with an ingest key (ksi_...) from Settings → Sites and keys. The key is a secret: keep it on your servers and out of browsers and source control. It decides the organization and site, so the payload never names them.
curl -sS https://app.knockstat.com/v1/batch \
-H "Authorization: Bearer $KNOCKSTAT_INGEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"sent_at": "2026-10-13T12:00:00.120Z",
"events": [
{
"uuid": "0192f3b0-7c1a-7000-8000-5d1e8f3a9b21",
"event": "signup",
"timestamp": "2026-10-13T11:59:00Z",
"distinct_id": "user-42",
"properties": { "plan": "pro", "seats": 3 }
}
]
}'The answer is 202 with the count accepted and every rejection, by its position in events:
{ "accepted": 1, "rejected": [ { "index": 1, "field": "request.path", "error": "request.path is required" } ] }Valid events are stored even when others in the same batch are rejected. Do not resend accepted events, and do not resend rejected ones without fixing them. A 202 means each accepted event is durable. You can gzip the body with Content-Encoding: gzip.
Event fields
| Field | Required | Meaning |
|---|---|---|
event | yes | Your event name (signup), or $pageview for a server-side page view. Other names starting with $ are reserved. |
uuid | recommended | Your id for this event, ideally a UUID v7 made when the event happened. A resend with the same uuid is stored once. Ingest makes one if absent. |
timestamp | no | When it happened, RFC 3339. Absent means when ingest received it. Old timestamps are accepted; future ones are not (see limits). |
distinct_id | no | Your id for the user, at most 200 characters. Needed to follow people across days. |
properties | no | Flat object of strings, numbers, booleans or null. |
request | for $pageview | path (with the query string, required), and optionally host, referrer, user_agent and client_ip. |
sent_at on the batch is your clock when you sent it. It is recommended: ingest shifts every timestamp by the difference to its own clock, so a server with a skewed clock does not get events rejected.
For a server-side page view, add request.client_ip and request.user_agent from the visitor’s request. Ingest uses the address for the country and the daily visitor hash, then drops it. Without them a page view has no country, visitor or session. client_ip is honoured only on /v1/batch and /v1/logs.
Limits
| Limit | Value | Over the limit |
|---|---|---|
| Events per batch | 1 to 1,000 | Batch rejected (400) |
| Body size | 1 MiB compressed, 8 MiB once decompressed | 413 |
Log lines per /v1/logs request | 5,000 | 400 |
| Event name | 1 to 200 characters, no control characters | Event rejected |
| Properties per event | 64 | Event rejected |
| Property key | 1 to 128 characters | Event rejected |
| Property value | string (at most 1,024 characters), finite number, boolean or null; no objects or arrays | Event rejected |
distinct_id | 200 characters | Event rejected |
timestamp | At most 10 minutes ahead of ingest's clock, after sent_at correction | Event rejected |
request.path, referrer, user_agent, host | 4,096, 2,048, 1,024 and 253 characters | Truncated, not rejected |
Characters are Unicode scalar values, not bytes.
Status codes and errors
| Code | Meaning | What to do |
|---|---|---|
202 | Accepted. The body lists accepted and rejected. | Drop the accepted events from your buffer. |
204 | Beacon received, whatever the outcome (/v1/e only). | Nothing. |
400 | Body is not valid JSON, the batch is empty or too large, or the format is unknown. | Fix the request. Do not retry it unchanged. |
401 | Missing, unknown or revoked ingest key. | Stop and check the key. Do not retry. |
413 | Body over the size limit. | Split the batch and resend. |
415 | Unsupported Content-Encoding. | Send gzip or no encoding. |
429 | Over the key's rate limit (rate_limited), or the organization is past its plan's monthly quota (quota_exceeded). Retry-After gives seconds. | Keep the events, wait at least Retry-After, resend. |
503 | Ingest cannot store events right now. Retry-After is given. | Back off with jitter, keep events buffered, resend. |
Errors other than beacons share one body shape, where error is one of invalid_body, batch_too_large, unknown_format, unauthorized, payload_too_large, unsupported_encoding, rate_limited, quota_exceeded or unavailable:
{ "error": "rate_limited", "message": "ingest key ksi_7c0e… is over 500 requests per second" }Retries
Delivery is at least once, deduplicated by uuid: keep the same uuid on every resend of an event and a retry after a timeout does not double-count. Retry network errors, 408, 429 and 5xx with exponential backoff and jitter, waiting at least Retry-After. Do not retry 400, 401 or 415. Split a 413 batch in half. Rate limits are in the API reference.
Node SDK
@knockstat/node queues events in memory and sends them in gzipped batches with retries, uuids, sent_at and validation done for you. Node 18 or later, no dependencies, ESM and CommonJS.
npm install @knockstat/nodeimport { Knockstat } from "@knockstat/node"; // or require("@knockstat/node")
const ks = new Knockstat({ key: process.env.KNOCKSTAT_INGEST_KEY }); // ksi_...
ks.track("signup", { plan: "pro", seats: 3 }, { distinctId: "user-42" });track and pageview return at once; events are sent in the background, and nothing throws on bad input (problems go to onError). A server handles many users, so pass distinctId on each call. Call shutdown() before the process exits to send what is queued:
process.on("SIGTERM", async () => {
await ks.shutdown();
process.exit(0);
});A server-side page view for a rendered page:
ks.pageview(
{ path: req.originalUrl, host: req.hostname, referrer: req.get("referer") ?? "", userAgent: req.get("user-agent"), ip: req.ip },
{},
{ distinctId: req.user?.id },
);| Option | Default | Meaning |
|---|---|---|
key | required | Ingest key, ksi_.... |
host | https://app.knockstat.com | Ingest base URL; events go to <host>/v1/batch. |
flushAt | 100 | Send once this many events are queued (at most 1,000). |
flushIntervalMs | 5000 | Send whatever is queued at least this often. |
maxQueue | 10000 | Most events held in memory; past it the oldest are dropped and reported. |
maxRetries | 5 | Retries per batch after the first attempt. |
onError | console.warn | Called for every dropped or rejected event. |
Events are held in memory only, so a crash loses what is queued.
Stuck? Email [email protected].