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
Groups
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
PHP library
Check access
Receive webhooks
Persons and events
Custom payment providers

Check access

Gate server-side features on a person's perks with the PHP library.

$voidhash->persons->entitlements asks Voidhash which perks a person can use right now, on a configured client. Run it before you serve anything paid that a person can reach without your app: an export endpoint, a paid AI model, a download.

The client SDKs check access in the app to decide what to show, but anyone can call your API directly, so gate server-side resources here. Both read the same entitlements, so they agree.

Check a perk

hasActivePerk returns a bool. This Laravel route gates an export on premium:

routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Voidhash\VoidhashClient;

Route::get('/reports/export', function (Request $request, VoidhashClient $voidhash) {
    $hasPremium = $voidhash->persons->entitlements->hasActivePerk([
        'distinctId' => (string) $request->user()->id, // the user ID your app passes to identify()
        'perkSlug' => 'premium',
    ]);

    if (! $hasPremium) {
        return response()->json(['error' => 'premium_required'], 402);
    }

    return response()->json(buildExport($request->user()));
})->middleware('auth');

Passing both or neither of perkSlug and perkId throws an ApiException with the code configuration_error before any request.

hasActivePerk returns false for a distinct ID Voidhash has never seen and for a slug that matches no perk. Authentication, permission, server and network failures throw, never return false, so a wrong key can't look like "nobody has access". The key needs read access to persons. A slug and an ID cost the same: both are compared against the person's entitlements.

Read the grants

grantsByDistinctId returns grants with getPerkId(), getPerkSlug(), getSource(), getSourceId(), getSourcePersonId() and getExpiresAt():

$grants = $voidhash->persons->entitlements->grantsByDistinctId('user_123');

foreach ($grants as $grant) {
    if ($grant->getPerkSlug() === 'premium') {
        echo $grant->getSource(), ' ', $grant->getExpiresAt(); // subscription 2026-11-20T09:12:44.000Z
    }
}

Every grant returned is active now, one per perk, across every page of results. getSource() is subscription or purchase, and getSourcePersonId() can name a person merged into this one. The schema also allows the source manual, a fallback label Voidhash doesn't create today; treat it like any other active grant. Reading the grants fails with the code person_not_found for a distinct ID Voidhash has never seen, so you can tell "never seen" from "seen, bought nothing".

When you already hold the person's ID, call $voidhash->persons->getEntitlements('person_k2v9x4m8q1r3').

Handle errors

API failures throw ApiException, with getErrorCode() and getType():

use Voidhash\Exception\ApiException;
use Voidhash\VoidhashClient;

/** The premium expiry, null for no expiry, or false without access. */
function premiumUntil(VoidhashClient $voidhash, string $distinctId): string|null|false
{
    try {
        $grants = $voidhash->persons->entitlements->grantsByDistinctId($distinctId);
    } catch (ApiException $error) {
        if ($error->getErrorCode() === 'person_not_found') {
            return false; // Never identified, so nothing was bought.
        }
        if (in_array($error->getType(), ['unauthenticated', 'permission_denied'], true)) {
            throw new RuntimeException('The Voidhash secret key is invalid or lacks access.', previous: $error);
        }
        throw $error; // Server error: the answer is unknown, not "no".
    }

    foreach ($grants as $grant) {
        if ($grant->getPerkSlug() === 'premium') {
            return $grant->getExpiresAt();
        }
    }

    return false;
}

Network failures throw a PSR-18 ClientExceptionInterface; treat them as unknown too.

Environments

The secret key the client uses 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 option changes this, so give each deployment its own key: test on staging, production in production.

Caching and failures

hasActivePerk and grantsByDistinctId make live requests; the library doesn't cache. 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 PSR-18 ClientExceptionInterface, or an ApiException with type internal or unavailable, 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

  • Receive webhooks to clear cached answers when access changes.
  • Persons and events
  • Subscription lifecycle: when access starts and ends.

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousPHP libraryNextReceive webhooks

On this page

Check a perkRead the grantsHandle errorsEnvironmentsCaching and failuresNext steps