# In-App Purchases (IAP)


In-App Purchases allow you to sell virtual goods, currency, and other digital content directly within your Instant Game.

This guide covers everything from initial setup to advanced integration patterns, including product catalog management, SDK integration, server-side verification, and testing.

## How IAP works

When a player buys something in your game, the following happens:

1. Your game calls the Instant Games SDK to display available products.
2. The player selects a product and confirms the purchase through the Facebook payment dialog.
3. Facebook processes the payment using the player's saved payment method (credit card, PayPal, or other regional payment methods).
4. Your game receives a purchase confirmation with a signed receipt.
5. You verify the receipt server-side and deliver the purchased content.

The player never leaves your game during this process. The payment dialog is an overlay that appears within the Instant Games container, so the player stays in the game throughout the purchase.

## Platform support

In-App Purchases are supported on **all platforms** where Instant Games are available, including iOS, Android, and the web (facebook.com). On iOS, purchases are processed through Apple's billing system.

Regardless of platform, your game should always check for payment API availability before showing purchase UI, using `FBInstant.getSupportedAPIs()`. This ensures your game handles any edge cases where payments may be temporarily unavailable on a particular client or device.

## Product types

The current Instant Games Digital Fulfillment Catalog (DFC) flow supports two self-serve product types:

### Consumable products

Consumable products are items that can be used up and purchased again. After a player buys a consumable product, you must explicitly **consume** it via the SDK before the player can buy it again. Most virtual goods in games are consumables.

**Examples:** Coin packs, gem bundles, extra lives, energy refills, loot boxes, power-ups.

### Non-consumable (durable) products

Non-consumable products are permanent, one-time purchases that persist in the player's account indefinitely. Once purchased, a durable product cannot be bought again. You should **not** consume durable products.

**Examples:** Ad removal ("Remove Ads" upgrade), premium skins, character unlocks, full game unlocks, permanent level packs.

### Subscriptions - not supported

**Subscriptions are not currently supported in Instant Games.**

Subscriptions are recurring purchases that charge the player on a regular schedule (weekly, monthly, or another interval). The player is billed automatically until they cancel.

**Examples:** Monthly VIP membership, weekly battle pass, seasonal subscription with exclusive content.

## Setting up IAP

Before you can sell anything in your game, you need to complete several setup steps in the App Dashboard.

### Step 1: Enable IAP for your app

1. Go to the [App Dashboard](https://developers.facebook.com/apps/) and select your app.
2. Navigate to **Instant Games** in the left sidebar.
3. Under **In-App Purchases**, toggle the feature to **Enabled**.

### Step 2: Complete the IAP approval process

In-App Purchases require approval from the Facebook team before they are available to real players. To request approval:

1. In the App Dashboard, go to **Instant Games** > **In-App Purchases**.
2. Submit your app for IAP review. You will need to provide:
   - A description of what you are selling and how it integrates into your game.
   - Screenshots or a video showing the purchase flow in your game.
   - Your game must comply with all [Facebook Platform Policies](https://developers.facebook.com/policy/) and [Instant Games Policies](https://developers.facebook.com/docs/games/instant-games/policies).
3. The review typically takes a few business days. You will be notified of the outcome in the App Dashboard.

While you wait for approval, you can still develop and test your IAP integration using test purchases (see [Testing IAP](#testing-iap)).

### Step 3: Create a payout account and business

The following screenshot shows the Game Payments settings in the DFC dashboard:

To receive payments from your IAP revenue, you need to set up a payout account:

1. In the App Dashboard, go to **Settings** > **Payments**.
2. Follow the prompts to create or link a **Business** entity (this is a Facebook Business account associated with your developer account).
3. Add your bank account or other payout method.
4. Complete any required tax documentation (W-9 for US developers, W-8BEN for international developers, or equivalent).
5. Once your payout account is verified, you will begin receiving payments according to the payout schedule.

## Product catalog management

Your game's products are managed through the **Digital Fulfillment Catalog (DFC)** in the App Dashboard. This is where you create, edit, and manage every item available for purchase in your game.

### Creating a product

To add a new product:

1. In the App Dashboard, go to **Instant Games > In-App Purchases > Products**.
2. Click **Create Product**.
3. Fill in the product fields (described below).
4. Save the product.

### Product fields

Each product has the following fields:

| Field | Description | Example |
|-------|-------------|---------|
| **Product ID** | A unique string identifier for this product. This is the ID you use in SDK calls. Once set, it cannot be changed. Use a descriptive, namespaced format. | `coins_500`, `premium_unlock` |
| **Product Name** | The display name shown to the player in the purchase dialog. Keep it clear and concise. | "500 Gold Coins", "Remove Ads" |
| **Description** | A short description of the product shown to the player. Explain what they are getting. | "A bundle of 500 gold coins to spend in the shop." |
| **Price** | The price of the product in your base currency (typically USD). Facebook automatically converts this to the player's local currency. Prices must meet minimum price thresholds (typically $0.99 USD or equivalent). | `0.99`, `4.99`, `9.99` |
| **Product Type** | Whether this product is consumable or non-consumable (durable). | `CONSUMABLE`, `NON_CONSUMABLE` |

### Product catalog best practices

- **Use clear, descriptive Product IDs.** You cannot change them later. Use a naming convention like `category_item_quantity` (e.g., `currency_coins_500`, `powerup_shield_1`).
- **Write player-friendly names and descriptions.** These are shown in the Facebook payment dialog, so make them clear and appealing.
- **Offer a range of price points.** Include both low-cost impulse purchases ($0.99 - $1.99) and higher-value bundles ($4.99 - $19.99+) to capture different spending levels.
- **Test your catalog thoroughly.** Use test purchases to verify that every product displays correctly and delivers the right content.

### Managing the catalog via API (V1)

In addition to the App Dashboard, you can create, update, and delete products programmatically using the Instant Games IAP Catalog API. This is useful for automated workflows, bulk catalog updates, or CI/CD pipelines that manage your game's product catalog without requiring manual changes in the dashboard.

#### Prerequisites

- An **app access token** for authentication. You can find your tokens at [https://developers.facebook.com/tools/accesstoken](https://developers.facebook.com/tools/accesstoken). Your App Token appears as `GG|{app-id}|{app-token}`.
- **curl** or any HTTP client.

All requests are authenticated as the app. The server validates that the `{app_id}` in the URL matches the authenticated app based on the token. If they do not match, you will receive a `403 Forbidden` error.

#### Adding or updating a product

To create a new product or update an existing one, send a `PUT` request to the catalog items endpoint. If a product with the given `product_id` already exists, its editable fields will be updated. If it does not exist, a new product will be created.

```bash
curl -i -X PUT "https://api.facebook.com/instant-games/catalog/{app-id}/items" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {app-id}|{app-token}" \
  -H "X-API-Version: 1.0.0" \
  -d '{
    "product_id": "coins_500",
    "product_title": "500 Gold Coins",
    "product_description": "A bundle of 500 gold coins to spend in the shop.",
    "product_type": "consumable",
    "product_price_amount": "0.99"
  }'
```

The request body accepts the following fields:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `product_id` | string | Yes | Developer-defined unique product identifier within the app. |
| `product_title` | string | Yes | Display title shown to the player. |
| `product_description` | string | No | Display description shown to the player. |
| `product_type` | string | Yes | One of: `consumable`, `durable`. |
| `product_price_amount` | string | Yes | Decimal price string (for example, `"0.99"`). Must match a supported price point. |

Note: The price currency code is default USD.

On success, the API returns:

```
204 No Content
```

#### Deleting a product

To delete an existing product, send a `DELETE` request with the `product_id` in the URL path:

```bash
curl -i -X DELETE "https://api.facebook.com/instant-games/catalog/{app-id}/items/{product-id}" \
  -H "Authorization: Bearer {app-id}|{app-token}" \
  -H "X-API-Version: 1.0.0"
```

Replace `{product-id}` with the product identifier you defined when creating the product (the same ID returned by `FBInstant.payments.getCatalogAsync()` in the SDK).

On success, the API returns:

```
204 No Content
```

#### Common API error cases

| Status Code | Meaning | Recommended Action |
|-------------|---------|-------------------|
| `400 Bad Request` | Missing or invalid fields, unsupported `product_type`, or invalid price point/currency combination. | Check that all required fields are present and that the price matches an allowed catalog price point. |
| `403 Forbidden` | The `app_id` in the URL does not match the authenticated app. | Verify that your access token corresponds to the correct app. |
| `404 Not Found` | The `product_id` does not exist (delete only). | Confirm the product ID is correct and that the product has not already been deleted. |
| `5xx Server Error` | A transient server-side failure. | Retry the request after a short delay. |

### Managing the catalog via API (V2 — Dynamic Regional Pricing Beta)

The V2 API extends the catalog management endpoints with support for **Dynamic Regional Pricing**. When your app is opted into dynamic pricing, you can publish per-region copies of each catalog item with different USD price points for different regions (currently `united_states`, `apac_tier_1`/`apac_tier_2`/`apac_tier_3`, `latam_tier_1`/`latam_tier_2`/`latam_tier_3`, and `emea_tier_1`/`emea_tier_2`/`emea_tier_3`).

V2 also adds a new `GET` method that returns the full list of catalog items for an app per region. This is useful for verifying the current state of your catalog or for syncing with an external system of record.

**To use V2 catalog endpoints, you must include the `X-API-Version: 2.0.0` header in all requests.** Use V2 only if you are enrolled in the Dynamic Regional Pricing Beta or to use the new list endpoint.

#### Region semantics

The `region` field follows a single rule across all V2 methods:

- If your app is **opted into** dynamic regional pricing, `region` is **required** on every V2 request. Omitting it returns `400 Bad Request`.
- If your app is **not opted into** dynamic regional pricing, please use the V1 endpoints unless you want to use the new list endpoint, `region` is ignored.

Valid `region` values:

| Value | Description |
|-------|-------------|
| `united_states` | Default catalog (no regional suffix applied). |
| `apac_tier_1` | APAC tier 1 regional catalog. |
| `apac_tier_2` | APAC tier 2 regional catalog. |
| `apac_tier_3` | APAC tier 3 regional catalog. |
| `latam_tier_1` | LATAM tier 1 regional catalog. |
| `latam_tier_2` | LATAM tier 2 regional catalog. |
| `latam_tier_3` | LATAM tier 3 regional catalog. |
| `emea_tier_1` | EMEA tier 1 regional catalog. |
| `emea_tier_2` | EMEA tier 2 regional catalog. |
| `emea_tier_3` | EMEA tier 3 regional catalog. |

Currency is always `USD` for all regions in this iteration; only the `product_price_amount` varies between regions.

#### Adding or updating a product (V2)

To create or update a region-aware catalog item, send a `PUT` request to the V2 items endpoint. If the underlying SKU (the `product_id` you supply plus the appropriate regional suffix) already exists, its editable fields are updated; otherwise a new item is created.

```bash
curl -i -X PUT "https://api.facebook.com/instant-games/catalog/{app-id}/items" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {app-id}|{app-token}" \
  -H "X-API-Version: 2.0.0" \
  -d '{
    "product_id": "coins_500",
    "product_title": "500 Gold Coins",
    "product_description": "A bundle of 500 gold coins to spend in the shop.",
    "product_type": "consumable",
    "product_price_amount": "0.49",
    "region": "apac_tier_1"
  }'
```

The request body accepts the following fields:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `product_id` | string | Yes | Developer-defined unique product identifier within the app. The server adds the regional suffix internally — do not include it. |
| `product_title` | string | Yes | Display title shown to the player. |
| `product_description` | string | No | Display description shown to the player. |
| `product_type` | string | Yes | One of: `consumable`, `durable`. |
| `product_price_amount` | string | Yes | Decimal price string (for example, `"0.49"`). Must match a supported USD price point. |
| `region` | string | Yes | See the Region semantics table above for the supported enum values. **Required** if the app is opted into dynamic pricing. |

On success, the API returns `204 No Content`.

#### Deleting a product (V2)

To delete a region-aware catalog item, send a `DELETE` request with the `product_id` you defined in the URL path and the `region` as a query parameter:

```bash
curl -i -X DELETE "https://api.facebook.com/instant-games/catalog/{app-id}/items/{product-id}?region=apac_tier_1" \
  -H "Authorization: Bearer {app-id}|{app-token}" \
  -H "X-API-Version: 2.0.0"
```

`{product-id}` is the identifier you defined when creating the product. When the app is opted into dynamic pricing, you must include `region` to identify which regional variant to delete.

On success, the API returns `204 No Content`.

#### Listing products (V2)

To retrieve the catalog items for an app, send a `GET` request:

```bash
curl -i -X GET "https://api.facebook.com/instant-games/catalog/{app-id}/items?region=apac_tier_1" \
  -H "Authorization: Bearer {app-id}|{app-token}" \
  -H "X-API-Version: 2.0.0"
```

When the app is opted into dynamic pricing, `region` is **required** and the response contains only items that belong to that region (with the regional suffix already stripped from each `product_id`). When the app is not opted in, `region` is ignored and the response contains all items in the default catalog.

A successful response returns `200 OK` with a JSON array of catalog items:

```json
[
  {
    "product_id": "coins_500",
    "product_title": "500 Gold Coins",
    "product_description": "A bundle of 500 gold coins to spend in the shop.",
    "product_type": "consumable",
    "price_amount_cents": 49,
    "price_currency_code": "USD",
    "region": "apac_tier_1"
  }
]
```

The `region` field is included on each item only when the app is opted into dynamic pricing. The `product_id` is always the identifier you defined (no regional suffix).

#### V2 error cases

In addition to the V1 error cases, the V2 endpoints can return:

| Status Code | Meaning | Recommended Action |
|-------------|---------|-------------------|
| `400 Bad Request` (region required) | Your app is opted into dynamic regional pricing and the `region` field/query parameter was omitted. | Always include `region` on V2 requests for opted-in apps. |
| `400 Bad Request` (invalid region) | The supplied `region` is not one of the supported enum values. | See the Region semantics table above for valid values. |

## SDK integration

The Instant Games SDK provides a complete set of APIs for handling in-app purchases. All payment APIs are in the `FBInstant.payments` namespace.

### Checking IAP availability

Before showing any purchase UI, verify that the payments API is available on the current platform:

```javascript
// Check if the payments API is supported on this platform
function isIAPAvailable() {
  const supportedAPIs = FBInstant.getSupportedAPIs();
  return supportedAPIs.includes('payments.getCatalogAsync');
}
```

This check is a best practice for defensive coding. Although IAP is supported on all platforms (iOS, Android, and web), there may be edge cases where payments are temporarily unavailable on a particular client or device. If `payments` APIs are not supported, hide your purchase buttons and rely on ads for monetization.

### Initializing payments

Before making any payment calls, you must call `onReady` to signal that your game is prepared to handle payments:

```javascript
FBInstant.payments.onReady(function () {
  console.log('Payments are ready.');
  // Now you can safely call getCatalogAsync, purchaseAsync, etc.
});
```

The `onReady` callback fires when the payments system has finished initializing. Do not call any other payment methods before this callback fires.

### Getting the product catalog

Retrieve the list of products available for purchase:

```javascript
FBInstant.payments.getCatalogAsync().then(function (catalog) {
  catalog.forEach(function (product) {
    console.log('Product:', product.productID);
    console.log('  Title:', product.title);
    console.log('  Description:', product.description);
    console.log('  Price:', product.price);
    console.log('  Price Currency Code:', product.priceCurrencyCode);
    console.log('  Image URI:', product.imageURI);
  });
}).catch(function (error) {
  console.error('Failed to get catalog:', error);
});
```

Each product in the catalog includes the following fields:

| Field | Type | Description |
|-------|------|-------------|
| `productID` | `string` | The unique product identifier you created in the DFC. |
| `title` | `string` | The product name. |
| `description` | `string` | The product description. |
| `price` | `string` | The localized price string (e.g., "$4.99" or "3,99 EUR"). |
| `priceCurrencyCode` | `string` | The ISO 4217 currency code (e.g., "USD", "EUR"). |
| `priceAmount` | `string` | The numeric price amount without currency symbol. |
| `imageURI` | `string` | The product image URL, if provided. |

### Making a purchase

To initiate a purchase, call `purchaseAsync` with a purchase configuration:

```javascript
FBInstant.payments.purchaseAsync({
  productID: 'coins_500',
  developerPayload: 'optional_custom_data',
}).then(function (purchase) {
  console.log('Purchase successful!');
  console.log('  Product ID:', purchase.productID);
  console.log('  Purchase Token:', purchase.purchaseToken);
  console.log('  Signed Request:', purchase.signedRequest);

  // IMPORTANT: Verify the purchase server-side before delivering content.
  verifyPurchaseOnServer(purchase.signedRequest).then(function () {
    // Deliver the purchased content to the player
    deliverCoins(500);

    // If the product is consumable, consume it so it can be purchased again
    return FBInstant.payments.consumePurchaseAsync(purchase.purchaseToken);
  }).then(function () {
    console.log('Purchase consumed successfully.');
  });
}).catch(function (error) {
  // Handle errors (player cancelled, network error, etc.)
  console.error('Purchase failed:', error.code, error.message);
});
```

The `developerPayload` field is an optional string you can attach to the purchase. It is included in the signed receipt and can be used for your own tracking purposes (e.g., a session ID or order reference).

### Getting unconsumed purchases

When your game starts, you should check for any unconsumed purchases from previous sessions. This handles the case where a player bought something but the game crashed before the content was delivered:

```javascript
FBInstant.payments.getPurchasesAsync().then(function (purchases) {
  purchases.forEach(function (purchase) {
    console.log('Unconsumed purchase:', purchase.productID);

    // Verify and deliver the content, then consume the purchase
    verifyPurchaseOnServer(purchase.signedRequest).then(function () {
      deliverProduct(purchase.productID);
      return FBInstant.payments.consumePurchaseAsync(purchase.purchaseToken);
    }).then(function () {
      console.log('Recovered purchase consumed:', purchase.productID);
    });
  });
}).catch(function (error) {
  console.error('Failed to get purchases:', error);
});
```

**Always call `getPurchasesAsync` on game start.** This is critical for ensuring players receive what they paid for, even if something went wrong during a previous session.

### Consuming a purchase

For consumable products, you must call `consumePurchaseAsync` after verifying and delivering the product:

```javascript
FBInstant.payments.consumePurchaseAsync(purchaseToken).then(function () {
  console.log('Purchase consumed. Player can now buy this product again.');
}).catch(function (error) {
  console.error('Failed to consume purchase:', error);
});
```

**Do not consume non-consumable (durable) products.** Durable products should remain in the player's `getPurchasesAsync` list permanently, indicating they own the item.

## Complete purchase flow

Here is the recommended end-to-end purchase flow for a consumable product:

```javascript
// 1. Check if IAP is available
if (!FBInstant.getSupportedAPIs().includes('payments.getCatalogAsync')) {
  console.log('IAP not available on this platform. Hiding store.');
  hideStoreUI();
  return;
}

// 2. Wait for payments to be ready
FBInstant.payments.onReady(function () {

  // 3. Recover any unconsumed purchases from previous sessions
  FBInstant.payments.getPurchasesAsync().then(function (purchases) {
    purchases.forEach(function (purchase) {
      recoverPurchase(purchase);
    });
  });

  // 4. Get the product catalog and display the store
  FBInstant.payments.getCatalogAsync().then(function (catalog) {
    displayStore(catalog);
  });
});

// 5. When the player taps a "Buy" button:
function onBuyButtonClicked(productID) {
  FBInstant.payments.purchaseAsync({
    productID: productID,
  }).then(function (purchase) {
    // 6. Verify the purchase on your server
    return verifyAndDeliver(purchase);
  }).then(function (purchase) {
    // 7. Consume the purchase (for consumable products)
    return FBInstant.payments.consumePurchaseAsync(purchase.purchaseToken);
  }).then(function () {
    // 8. Update the game UI
    updateStoreUI();
    showSuccessMessage('Purchase complete! Enjoy your items.');
  }).catch(function (error) {
    if (error.code === 'USER_INPUT') {
      // Player cancelled the purchase - this is normal, not an error
      console.log('Player cancelled the purchase.');
    } else {
      console.error('Purchase error:', error.code, error.message);
      showErrorMessage('Something went wrong. Please try again.');
    }
  });
}
```

## Server-side verification

**Never trust the client.** Always verify purchases on your server before delivering content. Every purchase includes a `signedRequest` field that contains a cryptographically signed receipt you can validate.

### How signedRequest works

The `signedRequest` is a string with two parts separated by a period (`.`):

```
<encoded_signature>.<encoded_payload>
```

1. **Decode the payload** by Base64-decoding the second part (using URL-safe Base64: replace `-` with `+` and `_` with `/` before decoding).
2. **Verify the signature** by computing an HMAC-SHA256 hash of the payload using your App Secret, then comparing it to the decoded signature.
3. **Extract the purchase data** from the decoded JSON payload.

### Server-side verification example (Node.js)

```javascript
const crypto = require('crypto');

function verifySignedRequest(signedRequest, appSecret) {
  const [encodedSignature, encodedPayload] = signedRequest.split('.');

  // Decode the signature
  const signature = Buffer.from(
    encodedSignature.replace(/-/g, '+').replace(/_/g, '/'),
    'base64'
  );

  // Compute the expected signature
  const expectedSignature = crypto
    .createHmac('sha256', appSecret)
    .update(encodedPayload)
    .digest();

  // Compare signatures securely
  if (!crypto.timingSafeEqual(signature, expectedSignature)) {
    throw new Error('Invalid signature');
  }

  // Decode and return the payload
  const payload = Buffer.from(
    encodedPayload.replace(/-/g, '+').replace(/_/g, '/'),
    'base64'
  ).toString('utf8');

  return JSON.parse(payload);
}
```

### Verification payload fields

The decoded payload contains:

| Field | Description |
|-------|-------------|
| `product_id` | The product that was purchased. |
| `purchase_token` | A unique identifier for this purchase. |
| `purchase_time` | Timestamp of the purchase. |
| `developer_payload` | The optional custom data you passed to `purchaseAsync`. |
| `player_id` | The Instant Games player ID. |

## Handling consumables vs. durables

The key difference in how you handle these two product types:

### Consumable flow

1. Player purchases the product.
2. Verify the `signedRequest` on your server.
3. Deliver the content (e.g., add 500 coins to the player's balance).
4. Call `consumePurchaseAsync` to mark the purchase as consumed.
5. The product is now available for purchase again.

### Durable flow

1. Player purchases the product.
2. Verify the `signedRequest` on your server.
3. Deliver the content (e.g., unlock "Remove Ads" feature).
4. **Do not call `consumePurchaseAsync`.**
5. On future game starts, call `getPurchasesAsync` to check if the player owns this durable product, and apply the benefit accordingly.

```javascript
// Example: Check for durable purchases on game start
FBInstant.payments.getPurchasesAsync().then(function (purchases) {
  const ownedProducts = purchases.map(function (p) { return p.productID; });

  if (ownedProducts.includes('remove_ads')) {
    disableAds();
  }

  if (ownedProducts.includes('premium_skin_dragon')) {
    unlockSkin('dragon');
  }
});
```

## Error handling

IAP operations can fail for various reasons. Always handle errors gracefully:

| Error Code | Meaning | Recommended Action |
|------------|---------|-------------------|
| `USER_INPUT` | The player cancelled the purchase dialog. | Do nothing. This is normal behavior, not an error. |
| `NETWORK_FAILURE` | A network error occurred during the purchase. | Show a friendly error message and suggest the player try again. |
| `INVALID_PARAM` | An invalid product ID or parameter was passed. | Check your product IDs match the catalog. This usually indicates a bug. |
| `INVALID_OPERATION` | The operation is not allowed (e.g., payments not ready). | Ensure `onReady` has fired and that payments are supported on the current client. |
| `PAYMENTS_NOT_INITIALIZED` | Payment APIs were called before `onReady`. | Wait for `onReady` before calling payment methods. |
| `CLIENT_UNSUPPORTED` | The current client does not support payments. | Hide purchase UI and use ads for monetization. |

```javascript
FBInstant.payments.purchaseAsync({ productID: 'coins_500' })
  .then(function (purchase) {
    handleSuccessfulPurchase(purchase);
  })
  .catch(function (error) {
    switch (error.code) {
      case 'USER_INPUT':
        // Player chose to cancel - not an error
        break;
      case 'NETWORK_FAILURE':
        showMessage('Network error. Please check your connection and try again.');
        break;
      case 'INVALID_OPERATION':
      case 'CLIENT_UNSUPPORTED':
        showMessage('Purchases are not available on this device.');
        hideStoreUI();
        break;
      default:
        showMessage('Something went wrong. Please try again later.');
        console.error('Unexpected purchase error:', error.code, error.message);
    }
  });
```

## Testing IAP

You can test your entire IAP integration without spending real money using the **In-App Test Payment System**.

### In-app test payment system

The In-App Test Payment System provides a mock payment dialog that lets developers and testers simulate purchases — including both success and failure scenarios — without processing real payments. Test purchases are not reported in revenue APIs or payout reports.

**Requirements:**

- Instant Games SDK version **7.0 or later** (`7.0`, `7.1`, `8.0`, or `latest`).
- The user must have an app role (admin, developer, or tester) assigned in the App Dashboard.

#### Disabling test payments

Test payments are enabled by default for users with a role on the app. Only admins and developers of the game are able to disable test payments following these steps:

1. In the App Dashboard, go to **Game Payments** > **In-App Test Payment Settings**.
2. Toggle **In-App Test Payments Mode** to off. The IAP test mode applies only to your own account — other users and live players are not affected.

#### Using the test dialog

The following screenshot shows the Mock IAP Test dialog:

When IAP test mode is enabled, initiating a purchase in your game displays the **Mock IAP Test dialog** instead of the real payment dialog. From this dialog you can:

- Select **Succeed** to simulate a successful purchase. The SDK returns a mock `Purchase` object with test data.
- Select an error code (for example, **Invalid Operation**) to simulate a specific failure scenario.

The test dialog behaves like the real purchase flow from the SDK's perspective, so your `purchaseAsync`, `consumePurchaseAsync`, and `getPurchasesAsync` calls work normally.

**Note:** The Mock IAP Test dialog only simulates the initial part of the purchase flow, and does not add items into your getPurchasesAsync list — it generates a mock `Purchase` object and `signedRequest` for client-side testing. It does not complete the entire purchase flow, including server-side payment processing, webhook notifications, or actual financial transactions.

If you need to verify the real payment flow, click **Switch to production payment flow** in the test dialog to temporarily bypass test mode.

#### Test purchase tokens

Test purchases return a mock `purchaseToken` in the format `111111111<random_number>`. The random suffix ensures each test purchase has a unique token, which is important if your server uses the purchase token as an idempotency key.

### Adding testers

To let other people test IAP on your game without being app developers:

1. In the App Dashboard, go to **App Roles**.
2. Add the person as a **Tester** on your app. The tester can then see the mock IAP dialog.

### Testing checklist

Before going live with IAP, verify the following:

- All products appear correctly in your in-game store (correct names, descriptions, prices).
- Purchasing a consumable product delivers the correct content and the product can be purchased again.
- Purchasing a durable product delivers the correct content and persists across sessions.
- Unconsumed purchases are recovered correctly when the game restarts (`getPurchasesAsync`).
- Server-side verification succeeds and the decoded payload is correct.
- Purchases complete successfully on iOS, Android, and web.
- Error handling works correctly (cancel the purchase dialog, disconnect from the network, and so on).
- The in-game store displays localized prices from the catalog (not hardcoded prices).
- Both success and failure scenarios behave as expected using the In-App Test Payment System.

## Revenue share and payout schedule

Facebook charges a revenue share on all In-App Purchase transactions. The standard revenue share for Instant Games is **70/30** — you receive 70% of the gross revenue and Facebook retains 30%. This is consistent with industry standard rates for digital storefronts.

### Payout schedule

Payouts are processed on a **monthly basis**, typically within 21 days after the end of the calendar month in which the revenue was earned. For example, revenue earned in January would be paid out by approximately February 21.

Payouts are sent to the bank account you configured in your payout account setup. You can view your earnings and payout history in the App Dashboard under **Monetization** > **Payout**.

### Taxes

Facebook provides tax documentation (1099 forms for US developers) but does not provide tax advice. You are responsible for reporting and paying taxes on your game revenue according to the laws of your jurisdiction. Consult a tax professional if you have questions.

## Best practices for IAP design

### Offer a strong first purchase

A player's first purchase typically has the lowest conversion rate. To increase first-purchase conversion:

- Offer a **"Starter Pack"** or **"First Purchase Bonus"** priced below the value of its contents. For example, a bundle worth $5 in virtual currency for $0.99.
- Show this offer at a moment when the player clearly wants something (e.g., after running out of lives for the first time).
- Present the offer for a limited time ("Available for the first 24 hours only") so players have a reason to act.

### Price strategically

- **Low entry points** ($0.99 - $1.99) convert more players. Use these for consumables.
- **Mid-range bundles** ($4.99 - $9.99) work well for currency packs and value bundles.
- **Premium purchases** ($14.99 - $49.99) serve committed players. Make these feel like exceptional value compared to smaller bundles.
- Always show the **best value** label on your most profitable bundle to guide players toward it.

### Make purchases visible but not aggressive

- Place your store in an easily accessible location (e.g., a "Shop" button on the main menu).
- Use subtle prompts at natural moments (e.g., "Need more coins? Visit the shop!" after the player runs out).
- Never block gameplay with mandatory purchase screens. Players who feel forced will leave.

### Design consumables for repeat purchases

- **Currencies** (coins, gems, diamonds) are the most effective consumable because they create a flexible economy and encourage repeat purchases.
- **Energy/lives systems** create natural purchase moments when the player runs out.
- **Bundles** that combine multiple items ("100 coins + 3 lives + 1 shield") feel like better value and increase average order value.

### Support non-paying players

- Ensure that your game is enjoyable and completable without purchases. Players who feel pressured to pay will leave negative reviews and stop playing.
- Use rewarded ads as a free alternative to purchases (e.g., watch an ad for 50 coins, or buy 500 coins for $0.99).
- Non-paying players provide social value — they invite friends, compete on leaderboards, and fill your multiplayer sessions.

## Next steps

- **[IAP Payment Webhooks](https://developers.facebook.com/documentation/games/monetize/iap-webhooks)** — Set up server-to-server webhook notifications for purchase and refund events.
- **[In-App Ads](https://developers.facebook.com/documentation/games/monetize/in-app-ads/overview)** — Learn how to generate revenue from ad impressions in your game.
- **[Monetization Best Practices](https://developers.facebook.com/documentation/games/monetize/best-practices)** — Strategic guidance on combining IAP and ads for maximum revenue.
- **[SDK Reference](https://developers.facebook.com/documentation/games/sdk-reference)** — Complete API documentation for all `FBInstant.payments` methods.