Events.
The unit. Every event that crosses the gate gets one row: a stable ID, a type you control, an arbitrary JSON payload, and the source it came from. Stored once, immutable after.
The shape
Five fields you'll see everywhere.
{
"id": "evt_01HZK4...",
"type": "user.signup",
"received_at": "2026-05-21T14:03:11.512Z",
"source_id": "src_01HZK3...",
"payload": {
"email": "ada@example.com",
"plan": "growth",
"utm": { "source": "twitter", "campaign": "launch" }
}
}type is your namespace — pick a convention (order.placed, user.signup, support.ticket.opened) and stick to it. Pipelines match on this string; filter and transform branches read it via $event.type.
Durability
A 202 means it's safe.
When the ingest call returns 202, your event is durable — it will be processed, even if our downstream systems hiccup in the next second. You don't need to retry on success; you don't need to keep a local copy "just in case." That's the contract.
The SDK throws on anything that isn't a 202; treat the throw as "not yet accepted" and retry.
Idempotency
Send the same event twice, safely.
Pass an idempotencyKey in your ingest call (SDK) or as a body field (HTTP). If we see the same key within the dedupe window, the second send is a no-op — same response, no duplicate row. Retries are free; deliberate replays cost.
Reading them back
Three places.
The dashboard's /events page is the live tail. /events/{id} shows a single event plus every delivery that fanned out from it. Pipelines themselves read events through $event.payload.<path> — see templates.