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
| Address | https://api.hanzo.ai/v1/event |
| Method | POST |
| Operation | post_event |
| Auth | Authorization: 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 input, 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.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
distinctId | body | string | — | |
event | body | string | — | |
properties | body | object | — | |
properties.* | body | any | — | |
time | body | string | — | |
type | body | string | — | |
[].distinctId | body | string | — | |
[].event | body | string | — | |
[].properties | body | object | — | |
[].properties.* | body | any | — | |
[].time | body | string | — | |
[].type | body | string | — | |
batch | body | CaptureEvent[] | — | |
batch[].anonymousId | body | string | — | |
batch[].channel | body | string | — | |
batch[].clip | body | ClipBody | — | |
batch[].clip.bytes | body | integer | — | |
batch[].clip.duration | body | integer | — | |
batch[].clip.object | body | string | — | |
batch[].currency | body | string | — | |
batch[].distinctId | body | string | — | |
batch[].environment | body | string | — | |
batch[].error | body | Exception | — | |
batch[].error.frames | body | Frame[] | — | |
batch[].error.frames[].column | body | integer | — | |
batch[].error.frames[].file | body | string | — | |
batch[].error.frames[].function | body | string | — | |
batch[].error.frames[].line | body | integer | — | |
batch[].error.handled | body | boolean | — | |
batch[].error.message | body | string | — | |
batch[].error.stack | body | string | — | |
batch[].error.type | body | string | — | |
batch[].event | body | string | — | |
batch[].groupId | body | string | — | |
batch[].groupType | body | string | — | |
batch[].kind | body | string | — | |
batch[].level | body | string | — | |
batch[].library | body | string | — | |
batch[].libraryVersion | body | string | — | |
batch[].log | body | LogBody | — | |
batch[].log.body | body | string | — | |
batch[].log.number | body | integer | — | |
batch[].log.severity | body | string | — | |
batch[].messageId | body | string | — | |
batch[].metric | body | MetricBody | — | |
batch[].metric.labels | body | object | — | |
batch[].metric.labels.* | body | any | — | |
batch[].metric.name | body | string | — | |
batch[].metric.value | body | number | — | |
batch[].path | body | string | — | |
batch[].personId | body | string | — | |
batch[].product | body | string | — | |
batch[].productId | body | string | — | |
batch[].properties | body | object | — | |
batch[].properties.* | body | any | — | |
batch[].quantity | body | integer | — | |
batch[].refCode | body | string | — | |
batch[].referrer | body | string | — | |
batch[].release | body | string | — | |
batch[].resource | body | string | — | |
batch[].revenue | body | number | — | |
batch[].service | body | string | — | |
batch[].sessionId | body | string | — | |
batch[].signupWeek | body | string | — | |
batch[].site | body | string | — | |
batch[].span | body | SpanBody | — | |
batch[].span.duration | body | integer | — | |
batch[].span.id | body | string | — | |
batch[].span.kind | body | string | — | |
batch[].span.parent | body | string | — | |
batch[].span.status | body | string | — | |
batch[].span.trace | body | string | — | |
batch[].spanId | body | string | — | |
batch[].timestamp | body | string | — | |
batch[].traceId | body | string | — | |
batch[].type | body | string | — | |
batch[].url | body | string | — | |
batch[].utm | body | UTM | — | |
batch[].utm.campaign | body | string | — | |
batch[].utm.content | body | string | — | |
batch[].utm.medium | body | string | — | |
batch[].utm.source | body | string | — | |
batch[].utm.term | body | string | — | |
events | body | CaptureEvent[] | — | |
events[].anonymousId | body | string | — | |
events[].channel | body | string | — | |
events[].clip | body | ClipBody | — | |
events[].clip.bytes | body | integer | — | |
events[].clip.duration | body | integer | — | |
events[].clip.object | body | string | — | |
events[].currency | body | string | — | |
events[].distinctId | body | string | — | |
events[].environment | body | string | — | |
events[].error | body | Exception | — | |
events[].error.frames | body | Frame[] | — | |
events[].error.frames[].column | body | integer | — | |
events[].error.frames[].file | body | string | — | |
events[].error.frames[].function | body | string | — | |
events[].error.frames[].line | body | integer | — | |
events[].error.handled | body | boolean | — | |
events[].error.message | body | string | — | |
events[].error.stack | body | string | — | |
events[].error.type | body | string | — | |
events[].event | body | string | — | |
events[].groupId | body | string | — | |
events[].groupType | body | string | — | |
events[].kind | body | string | — | |
events[].level | body | string | — | |
events[].library | body | string | — | |
events[].libraryVersion | body | string | — | |
events[].log | body | LogBody | — | |
events[].log.body | body | string | — | |
events[].log.number | body | integer | — | |
events[].log.severity | body | string | — | |
events[].messageId | body | string | — | |
events[].metric | body | MetricBody | — | |
events[].metric.labels | body | object | — | |
events[].metric.labels.* | body | any | — | |
events[].metric.name | body | string | — | |
events[].metric.value | body | number | — | |
events[].path | body | string | — | |
events[].personId | body | string | — | |
events[].product | body | string | — | |
events[].productId | body | string | — | |
events[].properties | body | object | — | |
events[].properties.* | body | any | — | |
events[].quantity | body | integer | — | |
events[].refCode | body | string | — | |
events[].referrer | body | string | — | |
events[].release | body | string | — | |
events[].resource | body | string | — | |
events[].revenue | body | number | — | |
events[].service | body | string | — | |
events[].sessionId | body | string | — | |
events[].signupWeek | body | string | — | |
events[].site | body | string | — | |
events[].span | body | SpanBody | — | |
events[].span.duration | body | integer | — | |
events[].span.id | body | string | — | |
events[].span.kind | body | string | — | |
events[].span.parent | body | string | — | |
events[].span.status | body | string | — | |
events[].span.trace | body | string | — | |
events[].spanId | body | string | — | |
events[].timestamp | body | string | — | |
events[].traceId | body | string | — | |
events[].type | body | string | — | |
events[].url | body | string | — | |
events[].utm | body | UTM | — | |
events[].utm.campaign | body | string | — | |
events[].utm.content | body | string | — | |
events[].utm.medium | body | string | — | |
events[].utm.source | body | string | — | |
events[].utm.term | body | string | — | |
batch[].distinct_id | body | string | — | |
batch[].uuid | body | string | — | |
distinct_id | body | string | — | |
timestamp | body | string | — | |
uuid | body | string | — |
Response
| Status | Body | Meaning |
|---|---|---|
2XX | CaptureResult | Success |
2XX body — 2 fields.
| Field | In | Type | Always | Description |
|---|---|---|---|---|
accepted | body | integer | — | |
dropped | body | integer | — |
Failure carries the platform error shape — see Errors.
Examples
hanzo event createimport { Configuration, EventApi } from 'hanzoai';
const api = new EventApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postEvent();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import EventApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = EventApi(client).post_event()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.EventAPI.PostEvent(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, event_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = event_api::post_event(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.EventApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new EventApi(client).postEvent();curl -X POST https://api.hanzo.ai/v1/event \
-H "Authorization: Bearer $HANZO_API_KEY"The door declares no tool for event — tools/list on https://api.hanzo.ai/v1/mcp names the products it does reach. Use HTTP or an SDK.
How is this guide?