In-app purchases

Run App Store and Google Play purchase and consume flows, then read the result from an event.

Read results from events, not the call

Awaiting a bdk.iap.* call only confirms the command was sent — the purchase result arrives later on an event. Subscribe before you call.

Every purchase and consume takes { id, type }: id is the App Store / Play product identifier, and type is "product" (one-time / consumable) or "subscription". Use the Ios / Android method for the device you're on.

The payload key is id, not product_id.

Results from the device are unverified. Treat purchaseSuccess as "the store reported a transaction", then verify the receipt server-side before granting entitlements. See Verify in-app purchases.

Buy a product on iOS

Opens the App Store purchase sheet. The result lands on purchaseSuccess, or purchaseFailed on cancel or error — both carry { platform, data }.

import { createBdkNative } from "@bdk/native/browser";

const bdk = createBdkNative();

bdk.on("purchaseSuccess", ({ platform, data }) => {
  // data is the raw, UNVERIFIED store payload — send it to your server to verify.
  console.log("purchased on", platform, data);
});

bdk.on("purchaseFailed", ({ data }) => {
  console.warn("purchase failed or cancelled", data);
});

await bdk.iap.purchaseIos({ id: "pro_monthly", type: "subscription" });

Buy a product on Android

Opens the Google Play purchase sheet. Results surface on the same purchaseSuccess / purchaseFailed events; the platform field tells you which store they came from.

bdk.on("purchaseSuccess", ({ platform, data }) => {
  if (platform === "android") void verifyOnServer(data);
});

await bdk.iap.purchaseAndroid({ id: "coins_100", type: "product" });

Buy from one shared codebase

Pick the right method by platform when you ship to both stores. The call resolves with no effect off-device, so add a web fallback.

async function buy(id: string, type: "product" | "subscription") {
  const os = bdk.getDeviceInfo()?.deviceOS?.toLowerCase();
  if (os === "ios") return bdk.iap.purchaseIos({ id, type });
  if (os === "android") return bdk.iap.purchaseAndroid({ id, type });
  // Not running natively — show a web fallback / upsell.
}

await buy("pro_monthly", "subscription");

Consume a product so it can be bought again

Use for consumables (coins, lives, refills) — a consumable can't be repurchased until it's consumed.

Consuming on the device only updates local state. The authoritative consume for Google Play happens server-side — see Verify in-app purchases.

await bdk.iap.consumeAndroidPurchase({ id: "coins_100", type: "product" });

IAP events reference

Which event answers which call. Each carries the platform ("ios" or "android") and a data object with the store's purchase details — product id, transaction id, receipt data, and result/error codes. Treat data as the raw store payload and verify it server-side. bdk.on(...) returns an unsubscribe function.

EventFired by
purchaseSuccesspurchaseIos / purchaseAndroid succeeded
purchaseFailedpurchase cancelled or errored
receiptReceiveda store receipt / token was returned
const offs = [
  bdk.on("purchaseSuccess", (e) => console.log("success", e.platform, e.data)),
  bdk.on("purchaseFailed", (e) => console.log("failed", e.data)),
  bdk.on("receiptReceived", (e) => console.log("receipt", e.data))
];

// later
offs.forEach((off) => off());

Verify on the server

The step that actually grants entitlements. Send the data from purchaseSuccess / receiptReceived to your backend and validate it. See Verify in-app purchases for verifyIosReceipt, verifyAndroidReceipt, readAndroidReceipt, and server-side consumeAndroidPurchase.

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

// In your API handler, with the purchaseToken forwarded from the client:
const validation = await verifyAndroidReceipt({
  packageName: "com.example.app",
  productId: "pro_monthly",
  purchaseToken,
  productType: "subscription"
});

if (validation.isValid) {
  // Inspect validation.payload / validation.raw before granting entitlements.
}