# Webhooks

There are 2 ways to get notified of order status changes via webhooks:

1. Global webhook URL set in the merchant dashboard (applies to all orders)
2. Per-order webhookUrl field when creating an order (overrides global URL for that order)

{% hint style="info" %}
You can read how to set a global webhook endpoint [HERE](https://docs.fonbnk.com/server-to-server/getting-started)
{% endhint %}

When an order status changes, a POST request is sent to the webhook URL with the following payload type:

{% code overflow="wrap" expandable="true" %}

```typescript
type Webhook = {
  event: 'order-status-change';
  data: {
    order: {
      userId: string;
      userEmail: string;
      merchantOrderParams?: string;
      countryIsoCode: string;
      flow: FlowType;
      type: OrderType;
      source: Source;
      status: OrderStatus;
      deposit: {
        paymentChannel: PaymentChannel;
        currencyType: CurrencyType;
        currencyCode: string;
        cashout: {
          exchangeRate: number;
          exchangeRateAfterFees: number;
          amountBeforeFees: number;
          amountAfterFees: number;
          amountBeforeFeesUsd: number;
          amountAfterFeesUsd: number;
        };
      };
      payout: {
        paymentChannel: PaymentChannel;
        currencyType: CurrencyType;
        currencyCode: string;
        cashout: {
          exchangeRate: number;
          exchangeRateAfterFees: number;
          amountBeforeFees: number;
          amountAfterFees: number;
          amountBeforeFeesUsd: number;
          amountAfterFeesUsd: number;
        };
        transaction?: {
          meta?: {
            transactionHash?: string;
          };
        };
      };
      refund?: {
        paymentChannel: PaymentChannel;
        currencyType: CurrencyType;
        currencyCode: string;
        cashout: {
          exchangeRate: number;
          exchangeRateAfterFees: number;
          amountBeforeFees: number;
          amountAfterFees: number;
          amountBeforeFeesUsd: number;
          amountAfterFeesUsd: number;
        };
        transaction?: {
          meta?: {
            transactionHash?: string;
          };
        };
      };
      createdAt: Date;
      updatedAt: Date;
    };
    userKyc?: {
      passedKycType?: KycType;
      passedKycHash?: string; // unique KYC submission identifier
      latestKycType?: KycType;
      latestKycStatus?: KycStatus;
    };
  };
}
```

{% endcode %}

Example payload

{% code overflow="wrap" expandable="true" %}

```json
{
  "event": "order-status-change",
  "data": {
    "order": {
      "userId": "68df8fcb372f378356ef7568",
      "userEmail": "chauncey69@gmail.com",
      "merchantOrderParams": "01K6MMKBKC8CX4SMJAR49DX5RZ",
      "countryIsoCode": "NG",
      "flow": "regular",
      "type": "on_ramp",
      "source": "api",
      "status": "payout_successful",
      "deposit": {
        "paymentChannel": "bank",
        "currencyType": "fiat",
        "currencyCode": "NGN",
        "cashout": {
          "exchangeRate": 1460.2,
          "exchangeRateAfterFees": 1505.2969,
          "amountBeforeFees": 15054,
          "amountAfterFees": 14603,
          "amountBeforeFeesUsd": 10.309547,
          "amountAfterFeesUsd": 10.000685
        }
      },
      "payout": {
        "paymentChannel": "merchant_balance",
        "currencyType": "merchant_balance",
        "currencyCode": "USD",
        "cashout": {
          "exchangeRate": 1,
          "exchangeRateAfterFees": 1,
          "amountBeforeFees": 10,
          "amountAfterFees": 10,
          "amountBeforeFeesUsd": 10,
          "amountAfterFeesUsd": 10
        }
      },
      "createdAt": "2025-10-03T08:56:43.212Z",
      "updatedAt": "2025-10-03T08:57:03.247Z"
    }
  }
}
```

{% endcode %}

#### Webhook Verification

We send a signature with each webhook request to protect merchants from fraudulent requests. Each request should be verified using a secret provided in the merchant dashboard.

**The signature is sent in the&#x20;**<mark style="color:$warning;">**x-signature**</mark>**&#x20;HTTP header.**

The signature is computed as follows:

```pseudocode
x-signature === SHA256(SHA256(secret) + JSON.stringify(request.body))
```

**TypeScript example:**

```typescript
import { createHash } from 'crypto';

function verifyWebhookSignature(
  requestBody: any,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = createHash('sha256')
    .update(JSON.stringify(requestBody))
    .update(createHash('sha256').update(secret, 'utf8').digest('hex'))
    .digest('hex');
  
  return signature === expectedSignature;
}
```

#### Webhook Response Requirements

Your webhook endpoint must:

* Respond with HTTP status code **2xx** to acknowledge receipt
* Respond within **20 seconds** (requests taking longer will timeout)

Any other status code or timeout will be considered a failure.

#### Retry Policy

If your webhook endpoint fails to respond successfully:

* We will retry up to **10 times**
* Retries will be attempted with exponential backoff: 1sec, 2sec, 4sec, 8sec, 16sec, 32sec, 64sec, 128sec, 256sec, 512sec
* Webhooks can be viewed in the merchant dashboard
