> ## Documentation Index
> Fetch the complete documentation index at: https://developers.bumxpress.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive signed event callbacks when payins, payouts, or transfers change status.

Webhooks let Bumxpress push status changes to your backend instead of relying only on polling `GET` endpoints. Use them to update your internal records when payins, payouts, or transfers complete or fail.

## Configure your webhook

In the merchant dashboard you configure:

* **URL**: HTTPS endpoint on your backend, for example `https://api.yourapp.com/webhooks/bumxpress`
* **Signing secret**: Shared secret used to verify signatures

All webhook requests use:

```http theme={null}
POST /webhooks/bumxpress
Content-Type: application/json
X-Bumxpress-Signature: sha256=<hex_hmac>
X-Bumxpress-Timestamp: 1779705120
X-Bumxpress-Event: payout.completed
X-Bumxpress-Webhook-Id: 88888888-8888-8888-8888-888888888888
```

## Verify the signature

Signatures use an HMAC over the timestamp and raw request body:

```text theme={null}
sha256=HMAC_SHA256(webhook_secret, timestamp + "." + raw_request_body)
```

Verify signatures **before** parsing JSON or applying side effects.

```ts theme={null}
import crypto from "node:crypto";

export function verifyBumxpressWebhook({
  secret,
  timestamp,
  rawBody,
  signature,
}: {
  secret: string;
  timestamp: string;
  rawBody: string;
  signature: string;
}) {
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

  const expectedBuffer = Buffer.from(expected);
  const signatureBuffer = Buffer.from(signature);

  return (
    expectedBuffer.length === signatureBuffer.length &&
    crypto.timingSafeEqual(expectedBuffer, signatureBuffer)
  );
}
```

<Warning>
  Always verify `X-Bumxpress-Signature` using the raw request body. Reject requests when verification fails.
</Warning>

## Event types

Bumxpress emits a small set of payment events. Use the `event` and `data.type` fields to route handling logic.

```json theme={null}
{
  "id": "88888888-8888-8888-8888-888888888888",
  "event": "payin.completed",
  "created_at": "2026-06-08T10:40:00Z",
  "data": {
    "type": "payin",
    "id": "44444444-4444-4444-4444-444444444444",
    "object": { }
  }
}
```

Supported events:

* **payin.completed** / **payin.failed** (`data.type: "payin"`)
* **payout.completed** / **payout.failed** (`data.type: "payout"`)
* **transfer.payin.completed** / **transfer.payin.failed** (`data.type: "transfer"`)
* **transfer.payout.completed** / **transfer.payout.failed** (`data.type: "transfer"`)
* **transfer.completed** / **transfer.failed** (`data.type: "transfer"`)

Transfer leg events are sent as each leg reaches a terminal state. Aggregate transfer events are still sent when the full transfer reaches a final state. The `data.object` field contains a snapshot of the resource at event time, including amounts, status, operator, wallet impact, and timestamps.

## Example payloads

**Payin webhook**

```json theme={null}
{
  "id": "88888888-8888-8888-8888-888888888888",
  "event": "payin.completed",
  "created_at": "2026-06-08T10:40:00Z",
  "data": {
    "type": "payin",
    "id": "44444444-4444-4444-4444-444444444444",
    "object": {
      "id": "44444444-4444-4444-4444-444444444444",
      "status": "completed",
      "send_amount": "10000",
      "send_currency": "XAF",
      "receive_amount": "16",
      "receive_currency": "USD",
      "country_code": "CM",
      "operator_code": "CM_MTN_MOMO",
      "rail_type": "mobile_money",
      "wallet_impact": {
        "credit_amount": "16.00",
        "currency": "USD"
      },
      "completed_at": "2026-06-08T10:39:30Z"
    }
  }
}
```

**Payout webhook**

```json theme={null}
{
  "id": "99999999-9999-9999-9999-999999999999",
  "event": "payout.completed",
  "created_at": "2026-06-08T10:42:00Z",
  "data": {
    "type": "payout",
    "id": "66666666-6666-6666-6666-666666666666",
    "object": {
      "id": "66666666-6666-6666-6666-666666666666",
      "status": "completed",
      "send_amount": "25",
      "send_currency": "USD",
      "receive_amount": "15125",
      "receive_currency": "XAF",
      "operator_code": "CM_MTN_MOMO",
      "wallet_impact": {
        "debit_amount": "26.50",
        "currency": "USD"
      },
      "completed_at": "2026-06-08T10:41:30Z"
    }
  }
}
```

**Transfer webhook**

```json theme={null}
{
  "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "event": "transfer.completed",
  "created_at": "2026-06-08T11:30:00Z",
  "data": {
    "type": "transfer",
    "id": "77777777-7777-7777-7777-777777777777",
    "object": {
      "id": "77777777-7777-7777-7777-777777777777",
      "status": "completed",
      "payin_status": "completed",
      "payout_status": "completed",
      "send_amount": "100000",
      "send_currency": "XAF",
      "receive_amount": "98500",
      "receive_currency": "XOF",
      "corridor_code": "CM-CI-XAF-XOF",
      "payer_operator_code": "CM_MTN_MOMO",
      "recipient_operator_code": "CI_ORANGE_MOMO",
      "collection_completed_at": "2026-06-08T11:15:00Z",
      "disbursement_completed_at": "2026-06-08T11:28:00Z"
    }
  }
}
```

**Transfer payin leg webhook**

```json theme={null}
{
  "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
  "event": "transfer.payin.completed",
  "created_at": "2026-06-08T11:15:05Z",
  "data": {
    "type": "transfer",
    "id": "77777777-7777-7777-7777-777777777777",
    "object": {
      "id": "77777777-7777-7777-7777-777777777777",
      "status": "disbursing",
      "payin_status": "completed",
      "payout_status": "pending",
      "payin_amount": "100000",
      "payin_currency": "XAF",
      "payout_amount": "98500",
      "payout_currency": "XOF",
      "payer_operator_code": "CM_MTN_MOMO",
      "recipient_operator_code": "CI_ORANGE_MOMO",
      "collection_reference": "TXN_COLLECTION_123",
      "collection_completed_at": "2026-06-08T11:15:00Z"
    }
  }
}
```

**Transfer payout leg webhook**

```json theme={null}
{
  "id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
  "event": "transfer.payout.failed",
  "created_at": "2026-06-08T11:28:05Z",
  "data": {
    "type": "transfer",
    "id": "77777777-7777-7777-7777-777777777777",
    "object": {
      "id": "77777777-7777-7777-7777-777777777777",
      "status": "failed",
      "payin_status": "completed",
      "payout_status": "failed",
      "payin_amount": "100000",
      "payin_currency": "XAF",
      "payout_amount": "98500",
      "payout_currency": "XOF",
      "payer_operator_code": "CM_MTN_MOMO",
      "recipient_operator_code": "CI_ORANGE_MOMO",
      "disbursement_reference": "TXN_PAYOUT_123",
      "disbursement_failed_at": "2026-06-08T11:28:00Z"
    }
  }
}
```

## Delivery and retries

Return any **2xx** status code to acknowledge receipt. If your endpoint returns a non-2xx or times out, Bumxpress retries with exponential backoff:

| Failed attempt | Next retry |
| -------------- | ---------- |
| 1              | 1 minute   |
| 2              | 5 minutes  |
| 3              | 15 minutes |
| 4              | 1 hour     |
| 5              | 4 hours    |
| 6              | 12 hours   |

After the seventh failed attempt, delivery is marked failed.

## Recommended handler pattern

Implement your webhook handler with these rules:

1. **Verify** the HMAC signature.
2. **Deduplicate** using `X-Bumxpress-Webhook-Id` or event `id`.
3. **Update** your local record using `data.id` and `data.type`.
4. **Acknowledge** with `200 OK` only after the update succeeds.
5. **Fallback** to `GET /payins/{id}`, `GET /payouts/{id}`, or `GET /transfers/{id}` when you need to recover from missed events.
