Hanzo
OpenapiEvent

Capture product events into your org's warehouse

Stores pageviews, browser errors, identifies and custom commerce events as rows in the caller's own tenant, and answers a receipt {accepted, dropped} that…

POST /v1/event

Addresshttps://api.hanzo.ai/v1/event
MethodPOST
Operationpost_event
AuthAuthorization: Bearer $HANZO_API_KEY

Stores pageviews, browser errors, identifies and custom commerce events as rows in the caller's own tenant, and answers a receipt {accepted, dropped} that always totals what was sent — a beacon is never silently discarded.

THE STATUS SAYS WHETHER ANYTHING LANDED, so a green check can never mean an empty warehouse. 200 means at least one event was stored (or that nothing was sent), and a nonzero dropped beside a nonzero accepted is a PARTIAL batch, never a failed one — a batch is not refused whole for its worst element. If NOTHING was stored the request is an error, and it names the one thing that fixes it: 401 ingest_key_required when every event was refused for want of a credential (the same events land with a key), and 400 unroutable_events when the caller HAD capability and the body still named nothing storable.

ONE door for every wire a Hanzo surface emits, dispatched by the SHAPE of the body and never by a second path: a bare event object, a bare array of them, the {batch:[…]} / {events:[…]} envelope, the team console's snake_case array, and the PostHog wire (spelled distinct_id/api_key, which the canonical wire never uses). BATCH IS A BODY, NOT A PATH — there is no /v1/event/batch, because an array already is one.

WHAT THE CALLER PRESENTS DECIDES WHAT IT MAY WRITE, and the door itself grants nothing. A validated bearer or an org API key writes the full event at full fidelity. A PUBLISHABLE key (pk-, on Authorization: Bearer, x-hanzo-ingest-key, or ?ingest_key= for navigator.sendBeacon, which cannot set headers) does the same, and is the credential a browser bundle ships: it is deliberately NOT a secret, it resolves WHICH tenant a beacon belongs to and nothing more. A pk- never authenticates and can READ NOTHING — not this org's errors, not a lens, not any other route on this API — so a leaked one lets a stranger write into your stream, and never lets one read out of it. Reading these rows back always takes a real bearer. A Hanzo Team workspace token resolves its org at REDUCED capability: the signed account names the person, so a distinctId in the body cannot pin events on a colleague.

NO CREDENTIAL IS REFUSED: a write the server cannot attribute to a project is 401 ingest_key_required, and a credential that IS presented but resolves to no project is 403 ingest_key_unknown. Nothing is filed under a shared tenant — events nobody can read are worse than events nobody sent, because the caller is told it succeeded. A browser bundle therefore always ships a pk-, which is what /v1/event/tag.js takes.

A REDUCED principal — a Hanzo Team workspace token — writes through the PROJECTION into its own org: narrowed to what the SERVER can name (pageviews and errors, plus the closed autocapture vocabulary click,click, input, change,change, submit, $view), where every one of those names is resolved through a server-owned table and stored as that table's value, so the name on the wire is never the name in the row. Stripped, too, to the fields the projection names, so revenue, personId, groupId and every property but the element annotation cannot reach a row — and an exception is carried only on an error, never on an interaction, so a click cannot ship a stack trace into a row's attributes. It does NOT name the person: the signed account is the identity, so a distinctId in the body cannot pin events on a colleague. Everything refused is counted in dropped.

The projected lane alone is bounded: 413 over 64 KiB, 400 over 50 events, 429 on the per-client-IP and per-peer caps, and a DNT:1 or Sec-GPC:1 request stores nothing and says so in the receipt. Two stored values carry their own bounds on top, because a request cap does not bound one value: an element annotation over 2 KiB (or a trail over 32 steps) and an exception class over 256 bytes are dropped from the row, which still lands. Authenticated bodies are offered to the observability plane first, which claims LLM-observability ingestion batches and declines everything else.

Request

159 fields, body application/json.

FieldInTypeRequiredDescription
distinctIdbodystring
eventbodystring
propertiesbodyobject
properties.*bodyany
timebodystring
typebodystring
[].distinctIdbodystring
[].eventbodystring
[].propertiesbodyobject
[].properties.*bodyany
[].timebodystring
[].typebodystring
batchbodyCaptureEvent[]
batch[].anonymousIdbodystring
batch[].channelbodystring
batch[].clipbodyClipBody
batch[].clip.bytesbodyinteger
batch[].clip.durationbodyinteger
batch[].clip.objectbodystring
batch[].currencybodystring
batch[].distinctIdbodystring
batch[].environmentbodystring
batch[].errorbodyException
batch[].error.framesbodyFrame[]
batch[].error.frames[].columnbodyinteger
batch[].error.frames[].filebodystring
batch[].error.frames[].functionbodystring
batch[].error.frames[].linebodyinteger
batch[].error.handledbodyboolean
batch[].error.messagebodystring
batch[].error.stackbodystring
batch[].error.typebodystring
batch[].eventbodystring
batch[].groupIdbodystring
batch[].groupTypebodystring
batch[].kindbodystring
batch[].levelbodystring
batch[].librarybodystring
batch[].libraryVersionbodystring
batch[].logbodyLogBody
batch[].log.bodybodystring
batch[].log.numberbodyinteger
batch[].log.severitybodystring
batch[].messageIdbodystring
batch[].metricbodyMetricBody
batch[].metric.labelsbodyobject
batch[].metric.labels.*bodyany
batch[].metric.namebodystring
batch[].metric.valuebodynumber
batch[].pathbodystring
batch[].personIdbodystring
batch[].productbodystring
batch[].productIdbodystring
batch[].propertiesbodyobject
batch[].properties.*bodyany
batch[].quantitybodyinteger
batch[].refCodebodystring
batch[].referrerbodystring
batch[].releasebodystring
batch[].resourcebodystring
batch[].revenuebodynumber
batch[].servicebodystring
batch[].sessionIdbodystring
batch[].signupWeekbodystring
batch[].sitebodystring
batch[].spanbodySpanBody
batch[].span.durationbodyinteger
batch[].span.idbodystring
batch[].span.kindbodystring
batch[].span.parentbodystring
batch[].span.statusbodystring
batch[].span.tracebodystring
batch[].spanIdbodystring
batch[].timestampbodystring
batch[].traceIdbodystring
batch[].typebodystring
batch[].urlbodystring
batch[].utmbodyUTM
batch[].utm.campaignbodystring
batch[].utm.contentbodystring
batch[].utm.mediumbodystring
batch[].utm.sourcebodystring
batch[].utm.termbodystring
eventsbodyCaptureEvent[]
events[].anonymousIdbodystring
events[].channelbodystring
events[].clipbodyClipBody
events[].clip.bytesbodyinteger
events[].clip.durationbodyinteger
events[].clip.objectbodystring
events[].currencybodystring
events[].distinctIdbodystring
events[].environmentbodystring
events[].errorbodyException
events[].error.framesbodyFrame[]
events[].error.frames[].columnbodyinteger
events[].error.frames[].filebodystring
events[].error.frames[].functionbodystring
events[].error.frames[].linebodyinteger
events[].error.handledbodyboolean
events[].error.messagebodystring
events[].error.stackbodystring
events[].error.typebodystring
events[].eventbodystring
events[].groupIdbodystring
events[].groupTypebodystring
events[].kindbodystring
events[].levelbodystring
events[].librarybodystring
events[].libraryVersionbodystring
events[].logbodyLogBody
events[].log.bodybodystring
events[].log.numberbodyinteger
events[].log.severitybodystring
events[].messageIdbodystring
events[].metricbodyMetricBody
events[].metric.labelsbodyobject
events[].metric.labels.*bodyany
events[].metric.namebodystring
events[].metric.valuebodynumber
events[].pathbodystring
events[].personIdbodystring
events[].productbodystring
events[].productIdbodystring
events[].propertiesbodyobject
events[].properties.*bodyany
events[].quantitybodyinteger
events[].refCodebodystring
events[].referrerbodystring
events[].releasebodystring
events[].resourcebodystring
events[].revenuebodynumber
events[].servicebodystring
events[].sessionIdbodystring
events[].signupWeekbodystring
events[].sitebodystring
events[].spanbodySpanBody
events[].span.durationbodyinteger
events[].span.idbodystring
events[].span.kindbodystring
events[].span.parentbodystring
events[].span.statusbodystring
events[].span.tracebodystring
events[].spanIdbodystring
events[].timestampbodystring
events[].traceIdbodystring
events[].typebodystring
events[].urlbodystring
events[].utmbodyUTM
events[].utm.campaignbodystring
events[].utm.contentbodystring
events[].utm.mediumbodystring
events[].utm.sourcebodystring
events[].utm.termbodystring
batch[].distinct_idbodystring
batch[].uuidbodystring
distinct_idbodystring
timestampbodystring
uuidbodystring

Response

StatusBodyMeaning
2XXCaptureResultSuccess

2XX body — 2 fields.

FieldInTypeAlwaysDescription
acceptedbodyinteger
droppedbodyinteger

Failure carries the platform error shape — see Errors.

Examples

hanzo event create

Event API · All Hanzo APIs · Interactive reference

How is this guide?

On this page