Event capture
Send analytics events to Voidhash with a project secret key or a publishable key.
Event capture is how you send analytics events to Voidhash without a client SDK. Use it from your backend, or from custom clients that don't run one of the SDKs.
There are two endpoints. POST /i/v1/capture sends one event, and POST /i/v1/batch sends 1 to
100 events in a single request.
Host
Send events to the API host:
https://api.voidhash.com/i/v1The client SDKs send their events there. The server libraries default to
https://ingest.voidhash.com, which doesn't answer requests yet, so set their ingest option to
https://api.voidhash.com: ingestUrl in Node.js and PHP, WithIngestURL in Go and ingest_url
in Rust.
Choose a credential
Which credential you send depends on where the request comes from. A backend you control sends its
secret key in the x-secret-key request header, as x-secret-key: vh_sk_.... A distributed
client, such as an app or web bundle, can't hold a secret, so it sends the publishable key as a
token field in the JSON body, as "token": "vh_pk_...".
Send one or the other. Only the x-secret-key header may carry a secret key: a secret key in the
body token field is rejected with 401 unauthorized. A restricted secret key needs
Events: Write. The Voidhash SDKs may send the publishable key in their usual
x-publishable-key header instead of the body; when both are present, the body token wins.
x-secret-key grants access to the project. Only send it from a server you control.
Capture one event
From a backend, authorize with the x-secret-key header and send the event in the body:
curl https://api.voidhash.com/i/v1/capture \
-H "Content-Type: application/json" \
-H "x-secret-key: $VOIDHASH_SECRET_KEY" \
-d '{
"sent_at": "2026-08-14T12:00:01.000Z",
"uuid": "018f6d2e-4c3a-7b1d-9e5f-2a8c1b0d4e6f",
"event": "paywall_viewed",
"distinct_id": "user_123",
"properties": { "location": "onboarding" },
"context": { "platform": "ios", "app_version": "2.4.0" },
"timestamp": "2026-08-14T12:00:00.000Z"
}'From a distributed client, drop the header and put the publishable key in the body instead:
curl https://api.voidhash.com/i/v1/capture \
-H "Content-Type: application/json" \
-d '{
"token": "vh_pk_...",
"sent_at": "2026-08-14T12:00:01.000Z",
"uuid": "018f6d2e-4c3a-7b1d-9e5f-2a8c1b0d4e6f",
"event": "paywall_viewed",
"distinct_id": "user_123",
"properties": {},
"context": {},
"timestamp": "2026-08-14T12:00:00.000Z"
}'Every event includes uuid, event, distinct_id, properties, context and timestamp. The
request itself includes sent_at and exactly one credential.
Voidhash puts event times on its own clock. When sent_at differs from the time the request
arrived by 5 minutes or more, every event in it shifts by that difference, rounded to the minute.
Each time is then clamped to between 24 hours before and 5 minutes after arrival, so events held
for days count on the day before they arrived.
The uuid is the deduplication key. Generate it once per event and reuse the same value if you
retry, so a retried event is not counted twice.
Batch events
POST /i/v1/batch accepts the same event objects under an events array. The request has one
request-level sent_at and the same credential choice as a single capture:
{
"sent_at": "2026-08-14T12:00:05.000Z",
"events": [
{
"uuid": "018f6d2e-4c3a-7b1d-9e5f-2a8c1b0d4e6f",
"event": "paywall_viewed",
"distinct_id": "user_123",
"properties": {},
"context": {},
"timestamp": "2026-08-14T12:00:04.000Z"
}
]
}Clients using a publishable key add "token": "vh_pk_..." next to sent_at. One invalid event
rejects the whole batch with 400.
A successful request returns 202 with accepted and rejected counts. Events are rejected, not
stored, when they arrive while the end user withholds consent or from a bot, when the project's
ingest policy blocks them, or when they use a reserved event name. Log
the x-request-id response header so you can reference it when diagnosing ingestion.
Rejected after capture
An accepted event can still be rejected while Voidhash processes it. Studio's Data health page charts these by reason:
| Reason | Why |
|---|---|
schema_rejected | A blank distinct_id, one over 255 characters or with a NUL character; $set or $set_once that isn't an object; a non-Boolean $process_person_profile; or an $identify without a valid $previous_distinct_id. |
reserved_event_name | A client sent a name only Voidhash's servers may record, such as $purchase.completed or $experiment.exposed. |
policy_rejected | The ingest policy changed while the event was in processing. |
processing_failed | Processing kept failing for about a day. Contact support; these events can be replayed. |
Smaller requests
Both endpoints accept a body compressed with gzip: send it with Content-Encoding: gzip. The JSON
body may also be sent with Content-Type: text/plain. A browser sends such a request to another
origin without a preflight OPTIONS request first, which saves a round trip.
Analytics consent
Send x-analytics-consent: denied or x-analytics-consent: pending when the end user has not
granted usage analytics. Voidhash then stores none of the request's events: the response is still
202, with every event counted as rejected, so the client does not retry them. Omit the header,
or send granted, to record the events. The Voidhash SDKs send this header for you and stop
capturing while consent is withheld.
Limits
| Limit | Value | Over it |
|---|---|---|
| Events per batch | 100 | 429 with rate_limited |
| Body size | 5 MB, as sent and once decompressed | 413 with payload_too_large |
| Event size | 256,000 bytes, once stored | 413 with payload_too_large |
A batch over 100 events answers 429 even though no rate is involved; retrying it fails the same
way, so split it. Capture has no request rate limit unless one is set for your project. When it is
reached, the response is 429 with rate_limited, a Retry-After header in seconds, and the same
delay in milliseconds as retry_after_ms in the body, for browsers that can't read the header.
Capture turned off for a project also answers 429.
Retries
Retry network failures, 429, 500 and 503 responses with exponential backoff. Honor
Retry-After or retry_after_ms when present, and resend the same UUIDs so retries deduplicate.
Don't retry 400, 401 or 413 responses. Those indicate a problem with the request itself, so
fix it and send again.
Next steps
- Capture an event and Capture a batch of events: every field and error.
- Events: built-in events, properties and how Voidhash stores them.
- Authentication: the credentials capture takes.
- Check your data: watch events arrive and find rejected ones.
Something wrong or missing on this page? Tell us by email or on Discord.