Verify purchases

Verify in-app purchases server-side from any Node service.

Verify a Google Play purchase

Confirm a purchase token with Google Play. Authenticate with a service account or access token, passed in the call or via BDK_GOOGLE_PLAY_SERVICE_ACCOUNT_JSON / BDK_GOOGLE_PLAY_ACCESS_TOKEN. Returns { isValid, payload, errorMessage, raw }.

import { verifyAndroidReceipt } from "@bdk/native/server/iap";

const validation = await verifyAndroidReceipt({
  packageName: "com.example.app",
  productId: "pro_monthly",
  purchaseToken,
  productType: "subscription"
});

if (validation.isValid) {
  // Read state before granting — see the next section.
}

Check entitlement before granting

isValid: true only means the token was accepted — not that the user owns the product. Use readAndroidReceipt(input) to read purchase state, acknowledgement state, and expiry before granting anything.

Never grant an entitlement off isValid alone. An expired or unacknowledged purchase can still report isValid: true.

import { verifyAndroidReceipt, readAndroidReceipt } from "@bdk/native/server/iap";

const validation = await verifyAndroidReceipt({
  packageName: "com.example.app",
  productId: "pro_monthly",
  purchaseToken,
  productType: "subscription"
});

if (validation.isValid) {
  // Google's purchase response has no productId, so tag the entry with the
  // one you verified — readAndroidReceipt filters receiptData by productId.
  const product = readAndroidReceipt({
    productId: "pro_monthly",
    receiptData: [{ productId: "pro_monthly", ...(validation.raw as Record<string, unknown>) }]
  });

  if (Number(product.expiresDateMs) > Date.now() && product.acknowledgementState !== null) {
    grantEntitlement(product.id);
  }
}

Consume a one-time product

Mark a consumable product as consumed so it can be purchased again.

import { consumeAndroidPurchase } from "@bdk/native/server/iap";

await consumeAndroidPurchase({
  packageName: "com.example.app",
  productId: "coins_100",
  purchaseToken
});

Verify an App Store receipt

Validate an iOS receipt with Apple. Pass the base64 receipt, and your sharedSecret (or set BDK_IOS_INAPP_SHARED_SECRET). A production receipt that Apple flags as sandbox is retried against the sandbox endpoint automatically; set useSandboxFallback: false to opt out. Returns { isValid, resultData, errorData, raw }resultData holds Apple's full response on success, errorData the failure reason.

import { verifyIosReceipt } from "@bdk/native/server/iap";

const validation = await verifyIosReceipt({
  receipt,
  sharedSecret: process.env.APP_STORE_SHARED_SECRET
});

if (validation.isValid) {
  // resultData is Apple's full verifyReceipt payload — read it next.
} else {
  console.error(validation.errorData);
}

Read an App Store entitlement

isValid: true means Apple accepted the receipt, not that the entitlement is live. Pass the verification's resultData to readIosReceipt(input) to pull the latest purchase for a product — its purchase, original-purchase, and expiry timestamps — before granting.

import { verifyIosReceipt, readIosReceipt } from "@bdk/native/server/iap";

const validation = await verifyIosReceipt({
  receipt,
  sharedSecret: process.env.APP_STORE_SHARED_SECRET
});

if (validation.isValid) {
  const product = readIosReceipt({
    productId: "pro_monthly",
    receiptData: validation.resultData as Record<string, unknown>
  });

  if (Number(product.expiresDateMs) > Date.now()) {
    grantEntitlement(product.id);
  }
}

Verify inside an Express route

These are plain async functions, so they drop into any handler — Express, a Next API route, a Firebase Function, or any Node service.

import express from "express";
import { verifyAndroidReceipt } from "@bdk/native/server/iap";

const app = express();
app.use(express.json());

app.post("/iap/android/verify", async (req, res) => {
  const validation = await verifyAndroidReceipt({
    packageName: req.body.packageName,
    productId: req.body.productId,
    purchaseToken: req.body.purchaseToken,
    productType: req.body.productType
  });
  res.json(validation);
});

app.listen(3000);