Skip to main content
The External API lets you send events and user properties to Klaritics from any system that cannot use a client SDK — backend services, server-side jobs, data warehouses, CRMs, or third-party platforms. It is a plain request/response JSON API over HTTPS. You call it; nothing calls you back.
You may hear these endpoints called “webhooks”, because they are often wired up as the destination of another system’s webhook. There is no callback, no signature header, and no subscription model — just two POST endpoints authenticated with an API key.

Before you start

Each API key is scoped to a single project. To send data to a different project, use a key issued for that project.
The API key is a write credential for your analytics data. Keep it server-side. Never embed it in a mobile app, browser bundle, or anything shipped to end users — use the client SDKs for client-side tracking.

Endpoints

Base URL
The API is write-only. There are no endpoints to read events or user properties back, and no delete endpoints. Confirm the exact base URL for your environment with your implementation contact before going live.

Authentication

Send your API key as a bearer token on every /events and /users call:
Keys must be active. Enabling, disabling, or rotating a key in the dashboard takes effect immediately.

Request conventions

  • Send Content-Type: application/json on every request.
  • Bodies must be UTF-8 JSON. Unknown extra fields are ignored.
  • Requests are abandoned after 30 seconds. Set your client timeout to 30 seconds or less.
  • Every response includes an X-Request-Id header. Log it — it is the only handle support has for tracing a specific call.
  • Only 200 means the request was accepted:

Send events

The body is a JSON array of events — not an object wrapping an array. Send a single event as an array of one.

Event fields

At least one of user_id or device_id must be non-empty. If you send only one, Klaritics currently fills the other with the same value. Send both when you have both.

source_ip is mandatory

Events sent from your servers would otherwise be recorded with your server’s IP. Pass through the originating client IP — for example, the first address in X-Forwarded-For at your own ingress — so geographic enrichment is correct. If an event has no end user, such as a batch job, you still need to send a parseable value. Agree a convention with your implementation contact rather than inventing one per caller.

Property values

A batch is accepted or rejected as a whole

If any event in the batch is invalid, the entire batch is rejected with 400 and nothing is ingested. The response lists every failing field, indexed by position in your batch:
Fix or drop the failing events, then resend the batch.

Set user properties

Sets profile properties on one user per call. There is no batch form.
The field names are literally $user_id and $set, with the $ prefix.

How updates are applied

The update is a merge, not a replace:
  • Keys you send are created or overwritten.
  • Keys you don’t send are left untouched.
  • The profile is created on the first write.
There is no delete operation. To clear a property, set it to an empty string:
Events are enriched with the user’s properties as they are processed. Set a user’s properties before or alongside their events, or those events won’t carry the properties you expect.

Limits

That puts sustained event throughput at roughly 10,000 events/sec per project. If you need more, talk to your implementation contact before ramping up. /users is not rate limited, but pace large profile backfills rather than sending them as fast as possible. To stay under the limits, buffer events and send batches of up to 100, keep batches well under 1 MiB, and reuse HTTP connections.

Errors and retries

Branch on the HTTP status and on validation_errors[].field. Treat the message strings as human-readable, not stable identifiers.
Recommended handling
No Retry-After header is returned on 429. Back off exponentially — for example 250 ms, 500 ms, 1 s, 2 s, capped.

Retrying is safe

Duplicate events are removed later in the pipeline, so when in doubt, retry — a duplicate is recoverable, a gap is not. That includes retrying after a timeout or network error, when you can’t tell whether the request was accepted. Resending the same /users payload is naturally safe, since it merges the same values. Include a stable business identifier such as order_id in properties anyway. It makes your own reconciliation much easier.

Accepted is not the same as queryable

A 200 from /events means the batch was valid and accepted for ingestion. It reaches reports after normal pipeline lag, and there is no status endpoint to poll. A 200 from /users means the profile was saved. time is not bounded, so backfills and late events are accepted. Always send the real event time, not the time of the API call, or backfilled data lands on today’s date.

Health check

No authentication. Returns 200 {"message":"ok"} when healthy and 503 {"message":"health check failed"} otherwise. Use it as a reachability check, not as a replacement for your own monitoring.

Examples

Client with batching and retries


Gotchas

Send [{...}], not {"events":[{...}]}. An object returns 400 invalid request body.
Purchase Completed, purchaseCompleted, purchase-completed, 2fa_enabled, and _internal are all rejected. Use purchase_completed and two_fa_enabled. Events can’t be renamed afterwards, so settle the naming up front.
A seconds-based timestamp passes validation because it’s greater than 0, then silently lands in 1970. Multiply by 1000.
Numbers are parsed as JSON doubles, so integers beyond 2^53 aren’t exact. Send large IDs — Snowflake IDs, 19-digit order numbers — as strings.
A . in a key is treated as a path separator, and $ is reserved. Stick to [a-z0-9_] keys.
"properties": {}, or no properties field at all, is rejected. If an event genuinely has no attributes, send something meaningful such as {"source": "billing_service"}.
This API doesn’t call back or verify signatures. Put a small relay in between that authenticates with your API key and reshapes the third party’s payload into the schemas above.

Go-live checklist

  • Project ID and API key stored in a secret manager, never in source or client-side code
  • Base URL confirmed for your environment
  • Authorization: Bearer <key> and Content-Type: application/json on every request
  • Events batched — at most 100 per request, body under 1 MiB
  • Event names matching ^[a-z][a-z0-9_]*$, reviewed as a tracking plan
  • time sent as UTC epoch milliseconds, from the real event time
  • user_id and/or device_id on every event, consistent with your SDK-side IDs
  • source_ip set to the end user’s IP, not your server’s
  • Large IDs sent as strings; string values at most 1024 bytes
  • Retries with exponential backoff and jitter on 429, 5xx, timeouts, and network errors — no retries on 400 or 401
  • X-Request-Id logged for every response
  • User properties written before or alongside the events they should enrich
  • Client timeout of 30 seconds or less, with connection reuse

Next steps

Client SDKs

Track events from web and mobile apps.

Event naming

Naming conventions for events and properties.