# IAP Payment Webhooks


IAP Payment Webhooks provide a server-to-server notification system for your Instant Game. When a player completes an in-app purchase or a refund is processed, Facebook sends an HTTPS `POST` request to your server with details about the transaction.

Webhooks are the most reliable way to confirm that a purchase was successful and to detect refunds. They complement the client-side SDK flow by providing a server-side signal that is not affected by network interruptions, app crashes, or the player closing the game mid-purchase.

## Prerequisites

Before setting up IAP webhooks, ensure you have:

- An Instant Game app with **In-App Purchases enabled**.
- A publicly accessible **HTTPS endpoint** on your server that can receive both `GET` and `POST` requests.
- **Admin access** to your app in the App Dashboard (required to view the App Secret).

## Subscribing to Webhooks

You subscribe to IAP webhooks through the [App Dashboard](https://developers.facebook.com/apps/). Graph API subscription is not supported for IAP webhooks.

### Step 1: Navigate to Webhooks

1. Open the [App Dashboard](https://developers.facebook.com/apps/) and select your app.
2. Navigate to **Use Cases** in the left sidebar.
3. Click **Webhook** in the list of use cases.

### Step 2: Configure Your Callback URL

1. Enter your **Webhook Callback URL** — the public HTTPS endpoint where Facebook will send purchase and refund notifications.
2. Enter a **Verify Token** — an arbitrary string that you define. Facebook sends this token during the subscription verification step so your server can confirm the request originated from your configuration.

### Step 3: Find Your App Secret

Your App Secret is used to validate the authenticity of webhook payloads. To find it:

1. In the [App Dashboard](https://developers.facebook.com/apps/), select your app and go to **Settings > Basic**.
2. Your App Secret is displayed on this page. You must have an **admin** role on the app to view the App Secret.

Keep your App Secret confidential. Never expose it in client-side code or public repositories.

## Testing Your Webhook

Before saving your webhook subscription, you should test the callback settings:

1. Click the **Test** button in the webhook configuration panel. This sends a verification `GET` request to your endpoint containing the `hub.mode`, `hub.challenge`, and `hub.verify_token` parameters.
2. Your server must echo back the `hub.challenge` value in the response body. If your server responds correctly, the test passes.
3. After saving the webhook URL successfully, you can also send a **test payload** to your URL using the test button in the App Dashboard to verify that your server correctly receives and processes the `POST` payload.

If the test fails, verify that your endpoint is publicly accessible, uses HTTPS, and correctly handles the verification handshake described below.

## Your Callback Server

Your callback server must handle two types of requests from Facebook.

### Subscription Verification

When you add or modify a webhook subscription, Facebook makes a single HTTPS `GET` request to your callback URL with the following query string parameters:

| Parameter | Description |
|-----------|-------------|
| `hub.mode` | The string `"subscribe"`. |
| `hub.challenge` | A random string generated by Facebook. |
| `hub.verify_token` | The verify token you provided when creating the subscription. |

Your endpoint should:

1. **Verify the `hub.verify_token`** matches the token you configured. This confirms the request is related to your subscription.
2. **Echo back the `hub.challenge` value** in the response body. This confirms to Facebook that your server is ready to receive callbacks.

**Example (Node.js):**

```javascript
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode'];
  const token = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];

  if (mode === 'subscribe' && token === YOUR_VERIFY_TOKEN) {
    console.log('Webhook verified successfully.');
    res.status(200).send(challenge);
  } else {
    console.error('Webhook verification failed.');
    res.sendStatus(403);
  }
});
```

> **Note for PHP developers:** In PHP, dots and spaces in query parameter names are converted to underscores automatically. Access these parameters using `$_GET['hub_mode']`, `$_GET['hub_challenge']`, and `$_GET['hub_verify_token']`. See the [PHP manual](https://php.net/manual/en/language.variables.external.php) for details.

### Receiving Updates

After a successful subscription, Facebook sends an HTTPS `POST` request to your endpoint whenever a purchase succeeds or a refund is processed. Your server must respond with HTTP status code `200`.

> **Important:** Any HTTP response other than `200` is treated as an error. Facebook will retry sending the webhook — immediately at first, then with decreasing frequency over the next 24 hours. If your server does not respond correctly, you may receive the same update multiple times. Design your handler to be idempotent.

The request has a content type of `application/json` and the body contains a JSON-encoded payload with the transaction details.

**Note for PHP developers:** In PHP, to read the request body:

```php
$data = file_get_contents("php://input");
$json = json_decode($data);
```

The `hub.mode`, `hub.challenge`, and `hub.verify_token` parameters are not sent with update `POST` requests — they are only used during the initial subscription verification.

With every `POST` request, Facebook sends an `X-Hub-Signature-256` HTTP header containing the SHA256 signature of the request payload, using your App Secret as the key, prefixed with `sha256=`. You should validate this signature to verify the integrity and origin of the payload. See [Validating Payload Signatures](#validating-payload-signatures) below.

## V2 Webhook Payload

Every webhook notification contains a JSON payload with the following fields:

| Field | Type | Description |
|-------|------|-------------|
| `developer_payload` | string | The developer-provided payload string that was passed to `purchaseAsync` when the purchase was initiated. Use this for your own tracking purposes (e.g., order references, session IDs). |
| `payment_action_type` | string (enum) | The type of payment event. See [PaymentStatusWebhookEventType](#paymentstatuswebhookeventtype) for possible values. |
| `user_id` | int64 | The Facebook user ID of the player who made the purchase. |
| `product_id` | string | The product identifier as defined in your product catalog (e.g., `"com.game.gems_100"`). |
| `purchase_platform` | string (enum) | The platform where the purchase was processed. See [DigitalContentPlatform](#digitalcontentplatform) for possible values. |
| `purchase_price_currency` | string | The ISO 4217 currency code of the purchase price (e.g., `"USD"`, `"EUR"`). |
| `purchase_price_amount` | int64 | The purchase price amount in the **smallest currency unit** (e.g., `999` represents $9.99 USD in cents). |
| `purchase_token` | int64 | A unique token identifying this purchase. Use this token when calling the Consume Purchase API. |
| `env` | string (enum) | The payment environment. See [PaymentEnv](#paymentenv) for possible values. |
| `version` | string | The payload version identifier. Always `"V2"` for this payload format. |

## Enum Reference

### PaymentStatusWebhookEventType

Used by the `payment_action_type` field to indicate the type of payment event.

| Value | Description |
|-------|-------------|
| `PURCHASE_SUCCESS` | The purchase was completed successfully. The player has been charged. |
| `REFUND_SUCCESS` | A refund has been processed for this purchase. |

### DigitalContentPlatform

Used by the `purchase_platform` field to indicate where the purchase was processed.

| Value | Description |
|-------|-------------|
| `FB` | Facebook web payments |
| `GOOGLE` | Google Play billing |
| `APPLE` | Apple App Store billing |
| `BILLING` | Meta billing |
| `OC` | Oculus / Meta Quest |
| `EXTERNAL_PROVIDER_STRIPE` | Stripe external payment provider |
| `UNKNOWN` | Unknown platform |

### PaymentEnv

Used by the `env` field to indicate the payment environment.

| Value | Description |
|-------|-------------|
| `PROD` | Production environment — real payments. |
| `DEV` | Development / sandbox environment. |
| `DEV_EXTERNAL` | External development environment. |
| `TEST` | Test environment — no real charges are made. |

## Example Webhook Payloads

### Purchase Success

```json
{
  "entry": [
    {
      "id": "7362434450471825",
      "time": 1777339377,
      "changes": [
        {
          "developer_payload": "{\"hello\":\"world\"}",
          "payment_action_type": "PURCHASE_SUCCESS",
          "user_id": 12345,
          "product_id": "test_product_001",
          "purchase_platform": "FB",
          "purchase_price_currency": "USD",
          "purchase_price_amount": 999,
          "purchase_token": 999999999,
          "env": "DEV",
          "version": "V2",
          "field": "in_app_purchase"
        }
      ]
    }
  ],
  "object": "application"
}
```

### Refund

```json
{
  "entry": [
    {
      "id": "7362434450471825",
      "time": 1777339400,
      "changes": [
        {
          "developer_payload": "{\"hello\":\"world\"}",
          "payment_action_type": "REFUND_SUCCESS",
          "user_id": 12345,
          "product_id": "test_product_001",
          "purchase_platform": "FB",
          "purchase_price_currency": "USD",
          "purchase_price_amount": 999,
          "purchase_token": 999999999,
          "env": "DEV",
          "version": "V2",
          "field": "in_app_purchase"
        }
      ]
    }
  ],
  "object": "application"
}
```

## Handling Purchase Success

When your server receives a webhook with `payment_action_type` set to `PURCHASE_SUCCESS`, you must:

1. **Grant the purchased item to the player** — Deliver the virtual goods, currency, or content associated with the `product_id` if you have not already done so from the client-side SDK flow.
2. **Call the Consume Purchase API** — After granting the item, call `FBInstant.payments.consumePurchaseAsync` with the `purchase_token` from the webhook payload to mark the purchase as consumed. This is required for consumable products so the player can purchase the item again.

> **Critical:** If your game fails to call the Consume Purchase API — either from within the FBInstant SDK during the in-game purchase flow or after receiving the IAP webhook notification for a successful purchase — **an automatic refund will be triggered within 12 hours**. Always call consume purchase immediately after granting the item to the player to prevent unintended refunds.

**Example server-side handler (Node.js):**

```javascript
app.post('/webhook', async (req, res) => {
  // Validate the X-Hub-Signature-256 header first (see Security section)

  const payload = req.body;

  if (payload.payment_action_type === 'PURCHASE_SUCCESS') {
    // 1. Grant the purchased item to the user
    // 2. Call consume purchase API with payload.purchase_token
  }

  res.sendStatus(200);
});
```

## Handling Refunds

When your server receives a webhook with `payment_action_type` set to `REFUND_SUCCESS`, it means a refund has been processed for a previous purchase.

### What triggers a refund?

Refunds can be initiated by:

- The player requesting a refund through **Facebook**, **Google Play**, or **Apple App Store**.
- An online payment dispute filed by the player.
- An **automatic refund** triggered when the Consume Purchase API was not called within 12 hours of a successful purchase (see above).

Historically, refunds account for **less than 4%** of all transactions.

### Recommended handling

- **Use refund notifications for internal analytics and tracking.** Record which players received refunds, for which products, and the frequency of refunds to monitor your game's purchase health.
- **In-game handling of refunds is at your discretion.** You may choose to revoke the purchased item, adjust the player's balance, or take no in-game action — the decision depends on your game's economy and policies.

## Validating Payload Signatures

Every webhook `POST` request includes an `X-Hub-Signature-256` header that you should use to verify the request came from Facebook and was not tampered with.

The header value has the format `sha256=<hex_signature>`, where the signature is an HMAC-SHA256 hash of the raw request body using your App Secret as the key.

**Example (Node.js):**

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

function verifyWebhookSignature(req, appSecret) {
  const signature = req.headers['x-hub-signature-256'];
  if (!signature) {
    throw new Error('Missing X-Hub-Signature-256 header');
  }

  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', appSecret)
    .update(req.rawBody)
    .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error('Invalid webhook signature');
  }
}
```

> **Important:** Always use a constant-time comparison function (like `crypto.timingSafeEqual`) to prevent timing attacks when comparing signatures.

## Next Steps

- **[In-App Purchases](https://developers.facebook.com/documentation/games/monetize/in-app-purchases)** — Complete guide to setting up and integrating IAP in your Instant Game.
- **[SDK Reference](https://developers.facebook.com/documentation/games/sdk-reference/v8.0)** — Full API documentation for `FBInstant.payments` methods including `consumePurchaseAsync`.