Check access
Ask Voidhash over HTTP whether a person can use a perk.
An access check asks Voidhash which perks a person can use right now. Run it on your server before you serve anything paid that a person can reach without your app: an export endpoint, a paid AI model, a download.
Server or app
The SDKs check access in the app to decide what to show. That is enough for screens inside the app, but anyone can call your API directly. Gate server-side resources with a server-side check.
| Check in | Use it for |
|---|---|
| The app (SDK) | Showing or hiding features and paywalls in the UI. |
| Your backend | API routes, downloads, generated content, anything a person can request directly. |
Both read the same entitlements, so they agree.
Check a perk
An access check is two requests with a secret key that has read
access to persons. First, find the person by the distinct ID your
app passed to identify. $VOIDHASH_PROJECT_ID is the id in the projects list of
GET /api/v1/session.
curl "https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/persons?distinctId=user_123" \
-H "Authorization: Bearer $VOIDHASH_SECRET_KEY"The response is a page with at most one person. An empty data array means Voidhash has never seen
this distinct ID, so the person has no access: you can tell "never seen" from "seen, bought
nothing".
{
"data": [
{
"id": "person_k2v9x4m8q1r3",
"projectId": "proj_r3k8v2m1x9q4",
"distinctId": "user_123",
"email": "ada@example.com",
"name": "Ada",
"isIdentified": true,
"createdAt": "2026-10-20T09:00:00.000Z"
}
],
"pageInfo": { "endCursor": null, "hasNextPage": false }
}Then list the person's entitlements:
curl "https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/persons/person_k2v9x4m8q1r3/entitlements" \
-H "Authorization: Bearer $VOIDHASH_SECRET_KEY"{
"data": [
{
"perkId": "perk_v4m9q2x8k1r7",
"perkSlug": "premium",
"source": "subscription",
"sourceId": "sub_q9x2m7k4v1r8",
"sourcePersonId": "person_k2v9x4m8q1r3",
"expiresAt": "2026-11-20T09:12:44.000Z"
}
],
"pageInfo": { "endCursor": null, "hasNextPage": false }
}The person has access when a returned entitlement's perkSlug matches. Every entitlement listed
is active now, one per perk. If hasNextPage is true, pass endCursor as cursor to get the
rest, and check every page before you answer "no". If you store perk IDs, compare perkId
instead.
| Field | Description |
|---|---|
perkId | The perk's ID. |
perkSlug | The perk's slug, or null if the perk was deleted. |
source | What granted access: subscription or purchase. See the note below on manual. |
sourceId | The subscription or purchase that granted it. |
sourcePersonId | Who holds the source: this person, or a person merged into them. |
expiresAt | When access ends, or null if it doesn't expire. |
The schema also allows source: "manual", with sourceId: null. It is only the fallback label
for an entitlement that has neither a subscription nor a purchase behind it. Studio, the API, the
CLI and the MCP server can't grant perks by hand, so every entitlement Voidhash creates has a
subscription or purchase source. Treat a manual entitlement like any other active one.
Environments
The secret key decides which purchases count. A production key (vh_sk_…) sees production
purchases, plus store sandbox purchases while they
unlock production apps.
A test key (vh_sk_test_…) sees store sandbox, simulated and test mode purchases, never production
ones. No header changes this, so give each deployment its own key: test on staging, production in
production.
Handle errors
Errors use the API's error format. Treat each one as a failure, never as "no access", so a wrong key can't look like "nobody has access":
| Status | type | What to do |
|---|---|---|
401 and 403 | unauthenticated, permission_denied | Fix the key: it is invalid, or lacks read access to persons. |
429 | rate_limited | Back off; see Limits. |
5xx | internal, unavailable | The answer is unknown. See Caching and failures. |
Caching and failures
Each check is a live request. On a hot path:
- Cache the answer per person for a short time, such as 60 seconds.
- Clear a person's cached answer when a webhook for them arrives, so renewals, refunds and expirations apply at once.
- Treat a network error or
5xxas unknown, not as no access. Serve the last known answer, or fail the request and let the client retry. Locking out a paying person because of a timeout is worse than a slightly stale answer.
Next steps
- Subscription lifecycle: when access starts and ends.
- Receive webhooks: hear about changes as they happen.
- People and identity
Something wrong or missing on this page? Tell us by email or on Discord.