VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Other languages
Check access
Receive webhooks

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 inUse it for
The app (SDK)Showing or hiding features and paywalls in the UI.
Your backendAPI 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.

FieldDescription
perkIdThe perk's ID.
perkSlugThe perk's slug, or null if the perk was deleted.
sourceWhat granted access: subscription or purchase. See the note below on manual.
sourceIdThe subscription or purchase that granted it.
sourcePersonIdWho holds the source: this person, or a person merged into them.
expiresAtWhen 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":

StatustypeWhat to do
401 and 403unauthenticated, permission_deniedFix the key: it is invalid, or lacks read access to persons.
429rate_limitedBack off; see Limits.
5xxinternal, unavailableThe 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 5xx as 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.

PreviousOther languagesNextReceive webhooks

On this page

Server or appCheck a perkEnvironmentsHandle errorsCaching and failuresNext steps