# 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](/server-to-server/getting-started.md)
{% 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.fonbnk.com/server-to-server/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
