VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Essentials
API overview
Authentication
Event capture
Push device API
Webhooks
Webhook delivery
Webhook events
Projects & keys
Projects
API keys
Audit log
Customers & access
Persons
Products
Perks
Sandbox
Paywalls
Paywalls
Paywall locations
Components
Component registry
Assets
Experiments & flags
Experiments
Feature flags
Analytics & events
Analytics
Analytics alerts
Cohorts
Group types
Groups
Events
Ingest policy
VoidQL
Tracking domains
Ads
Ads
Ad accounts
Meta measurement
Payments & stores
Payment providers
Payment provider products
Purchase history imports
Payment sources
Webhooks & notifications
Webhooks
Push notifications
Push configurations
Automations
Organization & team
Organizations
Members
Invitations
Billing
Your account
Profile
Session
Notices
Connected apps
Terms of Service
Developer tools
Schema
SDK
Event capture
Capture an event
Capture a batch of events

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/v1

The 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.

Never expose a secret key

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:

ReasonWhy
schema_rejectedA 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_nameA client sent a name only Voidhash's servers may record, such as $purchase.completed or $experiment.exposed.
policy_rejectedThe ingest policy changed while the event was in processing.
processing_failedProcessing 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

LimitValueOver it
Events per batch100429 with rate_limited
Body size5 MB, as sent and once decompressed413 with payload_too_large
Event size256,000 bytes, once stored413 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.

PreviousAuthenticationNextCapture an event

On this page

HostChoose a credentialCapture one eventBatch eventsRejected after captureSmaller requestsAnalytics consentLimitsRetriesNext steps