Purchases

EntitleHub validates receipts and owns entitlements; the on-device purchase happens through a store billing library. On Expo / React Native that's one call; on any other stack it's a short backend hop.

Expo / React Native? One call.

@entitlehub/react-native opens the native store sheet, validates the receipt with EntitleHub, and returns your entitlements — the RevenueCat DX.

npx expo install expo-iap · npm i @entitlehub/react-native @entitlehub/sdk

ts
import { configureEntitleHub, purchaseProduct, isEntitled } from "@entitlehub/react-native";

await configureEntitleHub({ apiKey: "pk_live_…", appUserId: user.id });

// opens the native sheet → validates with EntitleHub → returns entitlements
const info = await purchaseProduct("app_pro_monthly", { isSubscription: true });
if (info.isActive("pro")) unlockPro();

Only your publishable key ships in the app — EntitleHub validates the receipt server-side, so a forged one is rejected. That's the whole integration. The rest of this page is the manual version for other stacks (or full control).

Native store-library crashes

The on-device purchase runs through a store library (expo-iap / OpenIAP). A native crash there (e.g. an EXC_BAD_ACCESSin the store module on a specific OS) is the store library's concern — a native exception off the JS thread can't be caught in JS. Levers: pin a working expo-iap version, pass your own module via configureEntitleHub({ iap }), or use the manual path below with a billing library you've verified on your target OS.

Manual — bring your own billing (any stack)

Keep a billing library for the purchase step, then report the receipt to EntitleHub yourself.

The loop

  1. App: open the native purchase sheet with your billing library → get a token/receipt.
  2. App → your backend: POST the token (never put the EntitleHub secret key in the app).
  3. Backend → EntitleHub: POST /v1/subscribers/{id}/purchases with your sk_ key — EntitleHub validates against Apple/Google and writes the grant.
  4. App: re-read entitlements (or let a webhook update your DB).

1. Install a billing library

bash
npx expo install expo-iap
# or:  npm install react-native-iap

2. App — buy, then send the receipt to your backend

tsx
import { requestPurchase, getProducts, finishTransaction } from "expo-iap";

async function buyPro(userId: string) {
  await getProducts(["app_pro_monthly"]);          // load from the store
  const purchase = await requestPurchase({ sku: "app_pro_monthly" });

  // Android gives a purchaseToken; iOS gives a StoreKit 2 JWS transaction.
  await fetch("https://your-backend.com/iap/report", {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: sessionToken },
    body: JSON.stringify({
      userId,
      productId: "app_pro_monthly",
      // Android:
      purchaseToken: purchase.purchaseToken,
      // iOS (StoreKit 2):
      jws: purchase.jwsRepresentation,
    }),
  });

  await finishTransaction({ purchase });           // acknowledge with the store
}

3. Backend — report to EntitleHub (validated)

With the SDK:

ts
import { EntitleHubServer } from "@entitlehub/sdk";
const eh = new EntitleHubServer({ apiKey: process.env.ENTITLEHUB_SECRET_KEY! });

app.post("/iap/report", async (req, res) => {
  const { userId, productId, purchaseToken, jws } = req.body;

  const info = jws
    ? // Apple: product/environment come from the verified JWS
      await eh.reportPurchase(userId, { store: "app_store", storeProductId: "", signedTransaction: jws })
    : // Google: EntitleHub validates the token with your Play service account
      await eh.reportPurchase(userId, { store: "play", storeProductId: productId, purchaseToken, isSubscription: true });

  res.json({ entitled: info.isActive("pro") });
});

Or the raw REST call — see the purchases endpoint.

4. App — read entitlements

tsx
import { EntitleHub } from "@entitlehub/sdk";
const eh = new EntitleHub({ apiKey: "pk_live_…", appUserId: userId });

const info = await eh.getCustomerInfo({ fetchPolicy: "network-only" }); // fresh after a purchase
if (info.isActive("pro")) unlockPro();

Restore purchases

Same shape: your billing library restores the platform's active purchases, you re-report each token to EntitleHub, then re-read. Because EntitleHub keys on your app_user_id, entitlements follow the user across reinstalls and devices.

Prerequisites

For validated reports you need store credentials configured on the project — a Play service accountfor Google; Apple works with no secret. Without a Play credential, a Google report falls back to the untrusted server-report mode (don't ship that).

Keeping state fresh

Renewals, cancellations, and refunds arrive automatically once you wire Apple ASSN / Google RTDN— no app involvement. Until then, re-report on app launch / restore to refresh the subscription's expiry.