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
Node.js library
Check access
Receive webhooks
Persons and events
Custom payment providers

Check access

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

voidhash.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 resolves with a boolean. This Express route gates an export on premium:

server/routes/export.ts
import express from "express";

import { voidhash } from "../voidhash";

export const router = express.Router();

router.get("/reports/export", async (req, res) => {
  const userId = currentUserId(req); // the user ID your app passes to identify()
  const hasPremium = await voidhash.entitlements.hasActivePerk({
    distinctId: userId,
    perkSlug: "premium",
  });

  if (!hasPremium) {
    return res.status(402).json({ error: "premium_required" });
  }

  res.json(await buildExport(userId));
});

Passing both or neither of perkSlug and perkId rejects with VoidhashNodeConfigurationError before any request. A slug costs one extra request to look up the perk, and needs read access to perks, so on a hot path pass perkId.

hasActivePerk resolves false for a distinct ID Voidhash has never seen and for a slug that matches no perk. Authentication, permission, server and network failures reject, never resolve false, so a wrong key can't look like "nobody has access". The key needs read access to persons.

Read the grants

getGrantsByDistinctId returns VoidhashEntitlementGrant objects with perkId, perkSlug, source, sourceId, sourcePersonId and expiresAt:

const grants = await voidhash.entitlements.getGrantsByDistinctId({ distinctId: "user_123" });

const premium = grants.find((grant) => grant.perkSlug === "premium");
console.log(premium?.source, premium?.expiresAt); // "subscription" "2026-11-20T09:12:44.000Z"

Every grant returned is active now, one per perk, across every page of results. source is subscription or purchase, and sourcePersonId 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.listEntitlements({ params: { personId } }).

Handle errors

getVoidhashApiError reads the code and type from a rejection:

import { getVoidhashApiError } from "@voidhash/node";

import { voidhash } from "./voidhash";

export async function premiumUntil(distinctId: string): Promise<string | null | false> {
  try {
    const grants = await voidhash.entitlements.getGrantsByDistinctId({ distinctId });
    const premium = grants.find((grant) => grant.perkSlug === "premium");
    return premium ? premium.expiresAt : false;
  } catch (error) {
    const apiError = getVoidhashApiError(error);
    if (apiError?.code === "person_not_found") {
      return false; // Never identified, so nothing was bought.
    }
    if (apiError?.type === "unauthenticated" || apiError?.type === "permission_denied") {
      throw new Error("The Voidhash secret key is invalid or lacks access.");
    }
    throw error; // Network or server error: the answer is unknown, not "no".
  }
}

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 getGrantsByDistinctId 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 rejection that isn't an API error, or one 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.

PreviousNode.js libraryNextReceive webhooks

On this page

Check a perkRead the grantsHandle errorsEnvironmentsCaching and failuresNext steps