Sell products & subscriptions

List store products, start a purchase, restore entitlements, and consume consumables from the App Store and Google Play.

Load your product catalog

Fetch localized products for a paywall. Unknown store ids come back in invalidIds. Each product is a BdkIapProductkind is consumable, nonConsumable, subscription, or nonRenewingSubscription.

In a plain browser the command doesn't run; the returned object has no ok field and triggered: false.

This feature can be switched off in a given app build. Check await bdk.capabilities.has("iap") before showing the paywall. When it is off, the call resolves ok: false with code: "common/feature_disabled". See Detect features.

The previous-generation purchase flow (purchaseIos / purchaseAndroid) is documented at In-app purchases and remains supported.

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

const bdk = createBdkNative();

if (!(await bdk.capabilities.has("iap"))) {
  // Hide the paywall — this build does not include the store.
}

const result = await bdk.iap.products(["coins_100", "pro_monthly"]);

if (!result.ok) {
  // Disabled, or not running in the app (no `ok`, `triggered: false`).
  return;
}

for (const product of result.products) {
  console.log(product.id, product.kind, product.price.formatted);
}

console.log("unknown ids", result.invalidIds);

Start a purchase

Open the store sheet. The call resolves { state: "launched" } — that is not a sale. The outcome arrives on iap.purchaseCompleted, so subscribe before you call.

Pass planId and offerId from the product's subscription plans when you buy a subscription.

PropertyTypeDescription
idstring · requiredThe App Store / Play product identifier.
planIdstringSubscription plan to buy.
offerIdstringOffer on that plan.
replacesstringAndroid product id of the active subscription this purchase replaces.
requestIdstringOptional correlation id, at most 64 characters.

Subscribe to iap.purchaseCompleted before you call. A deferred grant re-emits later — keep a persistent listener, not a one-shot.

bdk.on("iap.purchaseCompleted", (event) => {
  if (event.state === "purchased") unlock(event.transaction?.productId);
  if (event.state === "pending") showWaitingForApproval();
});

const result = await bdk.iap.purchase({
  id: "pro_monthly",
  planId: "monthly",
  offerId: "trial"
});

if (!result.ok) return;
// result.state === "launched" — the store sheet opened. Wait for the event.

Purchase and wait

Wait for the first outcome (purchased, pending, cancelled, or failed). pending is deferred approval — the grant arrives later on iap.purchaseCompleted, so keep a persistent listener.

purchaseAndWait takes the same options as purchase, plus timeoutMs to bound the wait for that first outcome (the store sheet, not a later deferred grant).

A pending result is not a grant. Do not unlock. The persistent listener receives the later purchased event.

bdk.on("iap.purchaseCompleted", (event) => {
  // Deferred grants and later settlements land here.
  if (event.state === "purchased") unlock(event.transaction?.productId);
});

const result = await bdk.iap.purchaseAndWait({ id: "pro_monthly" });

if (!("state" in result)) {
  // Disabled, timed out, or not running in the app.
  return;
}

if (result.state === "purchased") unlock(result.transaction?.productId);
if (result.state === "pending") showWaitingForApproval();
if (result.state === "cancelled") return;
if (result.state === "failed") console.warn(result.code);

Check entitlements

Read what the store currently says the user owns. Each item is a BdkIapEntitlementstate is active, gracePeriod, billingRetry, revoked, or expired.

const result = await bdk.iap.entitlements();

if (!result.ok) return;

for (const entitlement of result.entitlements) {
  console.log(entitlement.productId, entitlement.kind, entitlement.state);
}

Restore purchases

Ask the store to restore previous purchases and subscriptions. Use it on a restore button. The call returns the restored entitlements.

const result = await bdk.iap.restore();

if (!result.ok) return;

for (const entitlement of result.restored) {
  console.log(entitlement.productId, entitlement.state);
}

Get the raw receipt

Read the material your server needs to verify a purchase. On iOS you get receipt plus jws; on Android you get purchases (productId, purchaseToken, packageName, productType).

Treat a client purchased event as the store reporting a transaction. Verify on the server before granting access — see Verify purchases.

const result = await bdk.iap.receipt();

if (!result.ok) return;

if (result.platform === "ios") {
  await sendToServer({ receipt: result.receipt, jws: result.jws });
} else {
  await sendToServer({ purchases: result.purchases });
}

Consume a consumable

Mark a consumable as used so the user can buy it again. Pass the product id. On iOS, remaining is the locally tracked count after the consume.

const result = await bdk.iap.consume("coins_100");

if (!result.ok) return;

console.log(result.consumed, result.remaining);

React to renewals and revocations

Listen for subscription renewals, revocations, and transactions that started outside your UI. Subscribe once at startup, then refresh entitlements.

bdk.on("iap.transactionUpdated", (event) => {
  // event.reason is "renewal", "revoked", or "external"
  void refreshEntitlements();
});