Events API
Behavioral event ingestion and identity stitching. Authenticates with either
credential tier — see the
Server-Side Event Tracking guide for
which key to use where. The machine-readable contract lives at
docs/api/events-openapi.yaml.
POST /v1/events
Ingests a single event (bare object) or a batch ({"events": [...]}).
Returns 202 once events are durably stored; segmentation runs
asynchronously.
Event fields
| Field | Type | Notes |
|---|---|---|
event_name | string | Required, 1–128 chars |
subscriber_id | string | Registry subscriber UUID |
anonymous_id | string | Pre-identify actor |
external_id | string | Server tier only — your own user id, resolved via the registry; unknown → 422 |
platform | string | Free-form; server SDKs default to server |
properties | object | ≤ 50 keys; keys and string values ≤ 256 chars |
occurred_at | RFC3339 | Defaults to receipt time; window: 48 h (client) / 90 d (server) |
idempotency_key | string | Unique per tenant; generated when omitted |
Exactly one actor identifier is required per event. The trust tier is
recorded as source from the credential; a source field in the payload is
ignored.
Limits
Client (X-SDK-Key) | Server (Bearer hober_srv_…) | |
|---|---|---|
| Events per request | 50 | 1000 |
| Request body | — | 500 KB (413 beyond) |
| Per event | — | 32 KB |
occurred_at window | 48 h | 90 days |
Responses
| Status | Meaning |
|---|---|
202 | { "accepted": N } — events stored |
401 | Missing, malformed, or revoked credential |
403 | Server key without the events:write scope |
413 | Server-tier body over 500 KB |
422 | Validation failure (batch caps, backdating window, unknown external_id, external_id on client tier, missing actor, property limits) |
GET /v1/events/export
Bulk export of the tenant's behavioral event history (the data-portability policy's self-serve path). Server key only — client SDK keys are publishable and cannot read history.
Query parameters
| Param | Type | Notes |
|---|---|---|
from / to | RFC 3339 | Time window on occurred_at; defaults to all history up to now |
cursor | string | Opaque keyset cursor from the previous page |
limit | int | 1..1000 (default 1000) |
Response
200 with the cursor envelope — next_cursor is empty when the page is not
full:
{
"items": [
{
"id": "…",
"subscriber_id": "…",
"anonymous_id": null,
"event_name": "order_placed",
"platform": "",
"properties": { "total_price": "19.99" },
"occurred_at": "2026-07-01T12:00:00Z",
"received_at": "2026-07-01T12:00:01Z",
"source": "server"
}
],
"next_cursor": ""
}
401 — missing/invalid credential. 403 — client-tier credential.
422 — malformed window, limit, or cursor.
Rows ordered by (occurred_at, id); pages are stable under concurrent
ingestion. Erased subscribers' rows appear with both actor ids null (GDPR
erasure anonymizes, it does not delete aggregates).
POST /v1/events/identify
Links an anonymous_id to a subscriber_id and backfills the subscriber
onto previously stored anonymous events.
Request
{ "anonymous_id": "anon-9f2c", "subscriber_id": "8a2f6c1e-…" }
Response
{ "linked": true, "backfilled": 12 }