docs / install / server

Server events and the HTTP API

Send events your backend knows about, such as sign-ups and purchases, with one authenticated POST. The Node SDK wraps it with batching and retries.

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

FieldRequiredMeaning
eventyesYour event name (signup), or $pageview for a server-side page view. Other names starting with $ are reserved.
uuidrecommendedYour 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.
timestampnoWhen it happened, RFC 3339. Absent means when ingest received it. Old timestamps are accepted; future ones are not (see limits).
distinct_idnoYour id for the user, at most 200 characters. Needed to follow people across days.
propertiesnoFlat object of strings, numbers, booleans or null.
requestfor $pageviewpath (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

LimitValueOver the limit
Events per batch1 to 1,000Batch rejected (400)
Body size1 MiB compressed, 8 MiB once decompressed413
Log lines per /v1/logs request5,000400
Event name1 to 200 characters, no control charactersEvent rejected
Properties per event64Event rejected
Property key1 to 128 charactersEvent rejected
Property valuestring (at most 1,024 characters), finite number, boolean or null; no objects or arraysEvent rejected
distinct_id200 charactersEvent rejected
timestampAt most 10 minutes ahead of ingest's clock, after sent_at correctionEvent rejected
request.path, referrer, user_agent, host4,096, 2,048, 1,024 and 253 charactersTruncated, not rejected

Characters are Unicode scalar values, not bytes.

Status codes and errors

CodeMeaningWhat to do
202Accepted. The body lists accepted and rejected.Drop the accepted events from your buffer.
204Beacon received, whatever the outcome (/v1/e only).Nothing.
400Body is not valid JSON, the batch is empty or too large, or the format is unknown.Fix the request. Do not retry it unchanged.
401Missing, unknown or revoked ingest key.Stop and check the key. Do not retry.
413Body over the size limit.Split the batch and resend.
415Unsupported Content-Encoding.Send gzip or no encoding.
429Over 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.
503Ingest 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/node
import { 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 },
);
OptionDefaultMeaning
keyrequiredIngest key, ksi_....
hosthttps://app.knockstat.comIngest base URL; events go to <host>/v1/batch.
flushAt100Send once this many events are queued (at most 1,000).
flushIntervalMs5000Send whatever is queued at least this often.
maxQueue10000Most events held in memory; past it the oldest are dropped and reported.
maxRetries5Retries per batch after the first attempt.
onErrorconsole.warnCalled for every dropped or rejected event.

Events are held in memory only, so a crash loses what is queued.

Stuck? Email [email protected].