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.
@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
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).
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
- App: open the native purchase sheet with your billing library → get a token/receipt.
- App → your backend: POST the token (never put the EntitleHub secret key in the app).
- Backend → EntitleHub:
POST /v1/subscribers/{id}/purchaseswith yoursk_key — EntitleHub validates against Apple/Google and writes the grant. - App: re-read entitlements (or let a webhook update your DB).
1. Install a billing library
npx expo install expo-iap
# or: npm install react-native-iap2. App — buy, then send the receipt to your backend
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:
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
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.
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.
