Authentication
The headers each Voidhash credential goes in, the endpoints that take it, and the errors it gets.
Every request to the Voidhash API carries one credential. This page covers how to send it. To create a secret key, restrict what it can do, roll it or make it expire, see API keys.
| Credential | Looks like | Header | Works with |
|---|---|---|---|
| Project secret key | vh_sk_…, or vh_sk_test_… for test | Authorization: Bearer | The management API and event capture |
| Sign-in token | vh_oat_…, or vh_cli_… from older CLI versions | Authorization: Bearer | The management API, as you |
| Publishable key | vh_pk_…, or vh_pk_test_… for test | x-publishable-key, with x-distinct-id | The client API the SDKs call, event capture |
A secret key grants access to its project. Send it only from a server you control, never from an app, a web page or a public repository.
Management API
The management API, under /api/v1, takes a project secret key or a sign-in token as a bearer
token:
curl https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/persons \
-H "Authorization: Bearer $VOIDHASH_SECRET_KEY"A secret key works in its own project only: a path naming another project answers 404, like
a project that doesn't exist. It reads and writes the environment it was created for, production
or test. A restricted key gets 403 for anything its access doesn't include.
A sign-in token acts as you, with your role in every project you can use. The CLI gets one when
you run voidhash login; an AI agent gets its own for the MCP server, which works only
there. A token reads production unless the request names another environment in the
x-environment header: production, test or all.
Some endpoints act for a person, such as /api/v1/me and renaming a project, and refuse a secret
key. Publishable keys don't work here.
Client API
The client API, under /api/v1/sdk, serves the SDKs in your end users' apps. It takes the
project's publishable key in x-publishable-key and the end user's distinct ID in x-distinct-id,
and every request also carries the headers that describe the app and the SDK, which the SDKs send
for you. Register devices explains each one.
curl https://api.voidhash.com/api/v1/sdk/person \
-H "x-publishable-key: vh_pk_..." \
-H "x-distinct-id: user_123" \
-H "x-client-bundle-id: com.example.app" \
-H "x-platform: ios" \
-H "x-platform-flavor: native" \
-H "x-sdk: ios" \
-H "x-sdk-version: 0.0.1-alpha.1" \
-H "x-is-debug-build: false" \
-H "x-is-backgrounded: false" \
-H "x-observer-mode: false"Use an SDK rather than calling the client API yourself. Sign-in tokens don't work here.
Distinct IDs outside printable ASCII
HTTP header values are limited to printable ASCII. When a distinct ID contains any other character,
such as an accented letter or an emoji, send its UTF-8 bytes percent-encoded and add
x-distinct-id-encoding: percent:
curl https://api.voidhash.com/api/v1/sdk/person \
-H "x-publishable-key: vh_pk_..." \
-H "x-distinct-id: Zo%C3%AB" \
-H "x-distinct-id-encoding: percent" \
-H "x-client-bundle-id: com.example.app" \
-H "x-platform: ios" \
-H "x-platform-flavor: native" \
-H "x-sdk: ios" \
-H "x-sdk-version: 0.0.1-alpha.1" \
-H "x-is-debug-build: false" \
-H "x-is-backgrounded: false" \
-H "x-observer-mode: false"Voidhash decodes the header only when that marker is present. Without it, the value is used exactly
as sent, so an ID that contains a literal % keeps its identity. A distinct ID is at most 255
characters and must not be blank or contain a NUL character.
Event capture
The event capture endpoints, /i/v1/capture and /i/v1/batch, take a credential in one of two
places:
- From your server, send the secret key in the
x-secret-keyheader, not as a bearer token. A restricted key needs Events: Write. - From an app or web page, send the publishable key as a
tokenfield in the JSON body. The SDKs may send it inx-publishable-keyinstead.
When both are present, the body token wins. A secret key in token is refused. See
Event capture for request examples.
Verify a credential
Call GET /api/v1/session with a secret key or a sign-in token. The response shows what kind of
credential it is, the person it acts for, and the organizations and projects it can use:
curl https://api.voidhash.com/api/v1/session \
-H "Authorization: Bearer $VOIDHASH_SECRET_KEY"Errors
The management API answers a refused credential with 401 and an unauthenticated error whose
code says why:
code | Meaning |
|---|---|
missing_credential | No credential was sent. |
malformed_authorization | The Authorization header isn't Bearer followed by a credential. |
invalid_api_key | The key is unknown or was deleted. |
expired_api_key | The key has expired; the message says when. |
invalid_token, session_ended | The sign-in token is invalid, has expired or was ended. Sign in again. |
publishable_key_not_allowed | A publishable key was sent; use a secret key. |
credential_not_accepted | This kind of credential doesn't work on this endpoint. |
A valid credential that can't do something gets 403 with permission_denied. Its code is
permission_denied when a key lacks the access, person_credential_required when only a person
can do it, and dashboard_session_required when only Studio can, as for disconnecting an app.
The client API answers a refused key with 401 and an Api/AuthenticationError body whose cause
holds the same codes, and a request with no key at all with Api/NotAuthenticatedError. A missing,
blank or malformed x-distinct-id is a 400 with an Api/SdkValidationError body, not a 401. Event capture answers 401 with the code unauthorized.
See Error codes for every code.
Next steps
- API keys: create a secret key, choose its access, roll it and set its expiry.
- API overview: base URL, conventions, errors and limits.
- Event capture: send events with a secret key or a publishable key.
- CLI: sign in once and call the API as yourself.
Something wrong or missing on this page? Tell us by email or on Discord.