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

# Webhooks

> Receive real-time HTTP callbacks for payment completions, refunds, disputes, and settlements. Learn how to verify signatures and handle events reliably.

Webhooks let Axra push payment events to your server the moment they happen, so you never need to poll the API to check whether a payment succeeded. When a payment event occurs — a charge completes, a dispute opens, or funds settle — Axra sends an HTTP POST request to the `webhookUrl` you configured in your business profile. Because many payment flows (like 3DS redirects or card network clearing) are asynchronous, you should always confirm payment status through webhooks rather than relying solely on the response from a charge request.

## Configure your webhook URL

Set your webhook endpoint in the Axra dashboard under **Settings → Business Profile**, or update it programmatically:

```bash theme={null}
PUT /business/config
```

```json theme={null}
{
  "webhookUrl": "https://your-domain.com/webhooks/axra"
}
```

<Note>
  Your webhook URL must be publicly reachable. During local development, use a tool like [ngrok](https://ngrok.com) to expose your local server.
</Note>

## Event types

Axra delivers the following events to your webhook endpoint:

| Event                             | Description                                                               |
| --------------------------------- | ------------------------------------------------------------------------- |
| `payment.created`                 | A payment record was created (before capture).                            |
| `payment.completed`               | Payment captured successfully.                                            |
| `payment.failed`                  | Payment processing failed.                                                |
| `payment.settled`                 | Funds cleared and available for withdrawal.                               |
| `payment.refunded`                | Payment has been refunded.                                                |
| `payment.disputed`                | A dispute (chargeback) has been opened.                                   |
| `payment.dispute_won`             | Dispute resolved in your favor.                                           |
| `payment.dispute_lost`            | Dispute resolved against you.                                             |
| `payment_link.session.completed`  | A hosted checkout session completed successfully.                         |
| `collection.created`              | A local-rails collection was created.                                     |
| `collection.processing`           | Funds have been detected, settlement is in progress.                      |
| `collection.completed`            | Funds settled; merchant USDC wallet credited.                             |
| `collection.failed`               | Collection failed (e.g. amount mismatch, provider rejection).             |
| `collection.expired`              | Virtual account expired without payment.                                  |
| `payout.initiated`                | A payout was accepted and the merchant wallet debited (status `CREATED`). |
| `payout.submitted`                | The payout was dispatched to the payout provider (status `SUBMITTED`).    |
| `payout.settled`                  | Funds were delivered to the recipient (status `SETTLED`).                 |
| `payout.failed`                   | The payout failed; the wallet debit was reversed (status `FAILED`).       |
| `payout.cancelled`                | A `CREATED` payout was cancelled before dispatch (status `CANCELLED`).    |
| `payout.review`                   | The payout is held for compliance/admin review (status `PENDING_REVIEW`). |
| `invoice.created`                 | An invoice was issued.                                                    |
| `invoice.paid`                    | An invoice was paid.                                                      |
| `recurring_gift.charge.succeeded` | A recurring-gift schedule successfully charged.                           |
| `recurring_gift.charge.failed`    | A recurring-gift charge attempt failed.                                   |

## Webhook payload structure

Every webhook request shares the same envelope format:

```json theme={null}
{
  "event": "payment.completed",
  "timestamp": "2026-03-20T14:30:05.000Z",
  "data": { ... }
}
```

The `data` object varies by event type. Below are the payloads for each event.

<AccordionGroup>
  <Accordion title="payment.created">
    Fires when a payment record is created — before the funds are captured. Useful for pre-populating order rows in your system.

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "amount": 49.99,
      "currency": "USD",
      "status": "PENDING"
    }
    ```
  </Accordion>

  <Accordion title="payment.completed">
    Fires when a payment is captured successfully. Update your order status to reflect payment received.

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "amount": "49.99",
      "currency": "USD"
    }
    ```
  </Accordion>

  <Accordion title="payment.failed">
    Fires when payment processing fails. The `reason` field contains the failure code.

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "reason": "card_declined"
    }
    ```
  </Accordion>

  <Accordion title="payment.settled">
    Fires when funds have cleared the card networks and are credited to your merchant wallet (minus fees).

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "amount": "49.99",
      "currency": "USD",
      "settledAt": "2026-03-22T10:00:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="payment.refunded">
    Fires when a refund is issued for a payment.

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "amount": 49.99,
      "currency": "USD",
      "refundId": "re_1Abc..."
    }
    ```
  </Accordion>

  <Accordion title="payment.disputed">
    Fires when a cardholder opens a dispute (chargeback). The full charge amount is debited from your account immediately, plus a non-refundable dispute fee.

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "disputeId": "dp_1Abc...",
      "disputeAmount": 49.99,
      "disputeFee": 5.82,
      "reason": "fraudulent"
    }
    ```
  </Accordion>

  <Accordion title="payment.dispute_won / payment.dispute_lost">
    Fires when a dispute is resolved. If you win, the charge amount is re-credited to your account (the dispute fee is not refunded). If you lose, no further changes occur — the funds were already debited.

    ```json theme={null}
    {
      "paymentId": "bpay_01H...",
      "won": true,
      "chargeAmount": 49.99
    }
    ```
  </Accordion>

  <Accordion title="collection.created">
    Fires immediately after a collection is created through the direct API or hosted checkout. `data.object` mirrors `GET /business/collections/{id}`.

    ```json theme={null}
    {
      "livemode": true,
      "object": {
        "id": "lcoll_01HVXYZ123456",
        "object": "collection",
        "status": "pending",
        "amount": 10000,
        "currency": "ngn",
        "country": "NG",
        "expiresAt": "2026-05-11T12:34:56Z",
        "channel": {
          "rail": "bank",
          "provider": "PAGA",
          "channelId": "ch_01HVNG7BANK"
        },
        "instructions": {
          "kind": "bank_transfer",
          "bankName": "Indulge MFB",
          "accountNumber": "9906154084",
          "accountName": "AXRA / Your Business",
          "reference": null
        },
        "metadata": { "orderId": "ord_1042" },
        "businessPaymentId": "bp_01HVXYZ123456",
        "source": "api",
        "createdAt": "2026-05-11T12:24:56Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="collection.processing">
    Fires when inbound funds are detected and settlement is in progress.

    ```json theme={null}
    {
      "livemode": true,
      "object": {
        "id": "lcoll_01HVXYZ789012",
        "object": "collection",
        "status": "processing",
        "amount": 5000,
        "currency": "kes",
        "country": "KE",
        "expiresAt": "2026-05-11T12:44:56Z",
        "channel": {
          "rail": "momo",
          "provider": "M-PESA",
          "channelId": "ch_01HVKE7MOMO"
        },
        "instructions": {
          "kind": "momo_prompt",
          "phone": "+254712345678",
          "prompt": "Approve the payment prompt from your mobile money provider."
        },
        "metadata": null,
        "businessPaymentId": "bp_01HVXYZ789012",
        "source": "api",
        "createdAt": "2026-05-11T12:34:56Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="collection.completed">
    Fires when the collection settles and your USDC wallet is credited. Fulfill the order on this event.

    ```json theme={null}
    {
      "livemode": true,
      "object": {
        "id": "lcoll_01HVXYZ345678",
        "object": "collection",
        "status": "completed",
        "amount": 2500,
        "currency": "zar",
        "country": "ZA",
        "expiresAt": "2026-05-11T12:49:56Z",
        "channel": {
          "rail": "eft",
          "provider": "Ozow",
          "channelId": "ch_01HVZA7EFT"
        },
        "instructions": {
          "kind": "hosted_page",
          "url": "https://pay.ozow.com/...",
          "provider": "Ozow"
        },
        "metadata": { "orderId": "ord_2048" },
        "businessPaymentId": "bp_01HVXYZ345678",
        "source": "session",
        "createdAt": "2026-05-11T12:39:56Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="collection.failed">
    Fires when the provider reports a terminal failure (for example, amount mismatch or rejection).

    ```json theme={null}
    {
      "livemode": true,
      "object": {
        "id": "lcoll_01HVXYZ901234",
        "object": "collection",
        "status": "failed",
        "amount": 10000,
        "currency": "ngn",
        "country": "NG",
        "expiresAt": "2026-05-11T12:34:56Z",
        "channel": {
          "rail": "bank",
          "provider": "PAGA",
          "channelId": "ch_01HVNG7BANK"
        },
        "instructions": {
          "kind": "bank_transfer",
          "bankName": "Indulge MFB",
          "accountNumber": "9906154084",
          "accountName": "AXRA / Your Business",
          "reference": null
        },
        "metadata": null,
        "businessPaymentId": "bp_01HVXYZ901234",
        "source": "api",
        "createdAt": "2026-05-11T12:24:56Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="collection.expired">
    Fires when deposit instructions expire before payment arrives.

    ```json theme={null}
    {
      "livemode": true,
      "object": {
        "id": "lcoll_01HVXYZ567890",
        "object": "collection",
        "status": "expired",
        "amount": 5000,
        "currency": "kes",
        "country": "KE",
        "expiresAt": "2026-05-11T12:44:56Z",
        "channel": {
          "rail": "momo",
          "provider": "M-PESA",
          "channelId": "ch_01HVKE7MOMO"
        },
        "instructions": {
          "kind": "momo_prompt",
          "phone": "+254712345678",
          "prompt": "Approve the payment prompt from your mobile money provider."
        },
        "metadata": null,
        "businessPaymentId": "bp_01HVXYZ567890",
        "source": "session",
        "createdAt": "2026-05-11T12:34:56Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="payment_link.session.completed">
    Fires when a hosted checkout / payment-link session is completed. `source` identifies the originator (`payment_link`, `invoice`, etc.) and `sourceId` is the originating resource ID.

    ```json theme={null}
    {
      "sessionId": "cs_01H...",
      "source": "payment_link",
      "sourceId": "plink_01H...",
      "status": "COMPLETED",
      "amount": 4999,
      "currency": "USD"
    }
    ```
  </Accordion>

  <Accordion title="invoice.created">
    Fires when a new invoice is issued.

    ```json theme={null}
    {
      "invoiceId": "inv_01H...",
      "invoiceNumber": "INV-0001",
      "status": "DRAFT",
      "totalAmount": 5000,
      "currency": "USD"
    }
    ```
  </Accordion>

  <Accordion title="invoice.paid">
    Fires when an invoice is paid via its checkout session.

    ```json theme={null}
    {
      "invoiceId": "inv_01H...",
      "invoiceNumber": "INV-0001",
      "sessionId": "cs_01H..."
    }
    ```
  </Accordion>

  <Accordion title="recurring_gift.charge.succeeded">
    Fires when a recurring-gift schedule successfully captures a charge. `chargeCount` is the cumulative number of successful charges on this schedule.

    ```json theme={null}
    {
      "recurringGiftId": "rg_01H...",
      "paymentLinkId": "plink_01H...",
      "amount": 5000,
      "currency": "USD",
      "chargeCount": 3,
      "paymentIntentId": "pi_01H..."
    }
    ```
  </Accordion>

  <Accordion title="recurring_gift.charge.failed">
    Fires when a recurring-gift charge attempt fails. `failureCount` tracks consecutive failures; the schedule is paused after the configured retry limit.

    ```json theme={null}
    {
      "recurringGiftId": "rg_01H...",
      "paymentLinkId": "plink_01H...",
      "error": "card_declined",
      "failureCount": 1
    }
    ```
  </Accordion>

  <Accordion title="payout.* (initiated / submitted / settled / failed / cancelled / review)">
    Fires across the payout lifecycle for outbound [local-rail payouts](/payments/payouts/local-rails). `data.object` is the same shape as `GET /v1/business/payouts/{id}`. Fulfill or reconcile your records on `payout.settled`; reverse on `payout.failed`.

    ```json theme={null}
    {
      "object": {
        "id": "po_01H...",
        "status": "SETTLED",
        "rail": "LOCAL_BANK",
        "amount": "100.00",
        "currency": "USDC",
        "recipientId": "rec_01H...",
        "destinationMasked": { "country": "NG", "accountNumber": "••••6789", "bankName": "Example Bank" },
        "providerRef": "yc_01H...",
        "settledAt": "2026-06-02T10:00:30.000Z",
        "quote": { "dstAmount": "151000", "rate": "1510", "axraFee": "0.50", "currency": "NGN", "country": "NG" }
      },
      "livemode": true
    }
    ```
  </Accordion>
</AccordionGroup>

## Webhook request headers

Axra includes these headers on every webhook request:

| Header             | Description                                   |
| ------------------ | --------------------------------------------- |
| `Content-Type`     | `application/json`                            |
| `X-Axra-Signature` | HMAC-SHA256 signature of the raw request body |
| `X-Axra-Event`     | The event type (e.g., `payment.completed`)    |

## Signature verification

<Warning>
  Always verify the `X-Axra-Signature` header before processing a webhook. Without verification, any party can send requests to your endpoint and trigger side effects in your system.
</Warning>

To verify a webhook:

<Steps>
  <Step title="Get your webhook secret">
    Find your `webhookSecret` in the Axra dashboard under **Settings → API Keys**. Store it as an environment variable — never hardcode it.
  </Step>

  <Step title="Compute the expected signature">
    Compute an HMAC-SHA256 digest of the **raw request body** (the bytes as received, before any JSON parsing) using your `webhookSecret` as the key.
  </Step>

  <Step title="Compare using constant-time equality">
    Compare the computed digest to the value in the `X-Axra-Signature` header using a constant-time comparison function. This prevents timing attacks.
  </Step>

  <Step title="Reject if signatures do not match">
    Return a `401 Unauthorized` response immediately. Do not process the event.
  </Step>
</Steps>

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(rawBody, signature, webhookSecret) {
    const expectedSignature = crypto
      .createHmac('sha256', webhookSecret)
      .update(rawBody)
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expectedSignature, 'hex'),
    );
  }

  app.post('/webhook', (req, res) => {
    const signature = req.headers['x-axra-signature'];
    const rawBody = JSON.stringify(req.body);

    if (!verifyWebhookSignature(rawBody, signature, process.env.AXRA_WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    const { event, data } = req.body;

    switch (event) {
      case 'payment.completed':
        // Update your order status
        break;
      case 'payment.settled':
        // Funds are now available
        break;
      case 'payment.disputed':
        // Alert your fraud team
        break;
    }

    res.status(200).send('OK');
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode(), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```

  ```go Go theme={null}
  func verifyWebhook(body []byte, signature, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(signature), []byte(expected))
  }
  ```
</CodeGroup>

<Note>
  Use the **raw** request body bytes for signature computation — not a re-serialized version of the parsed JSON. JSON serialization is not guaranteed to be deterministic, and any whitespace difference will cause verification to fail.
</Note>

## Retry policy

Axra considers a delivery successful when your endpoint responds with an HTTP `2xx` status code within 30 seconds. If delivery fails, Axra retries with the following schedule:

| Attempt | Delay after failure |
| ------- | ------------------- |
| 1       | Immediate           |
| 2       | 30 seconds          |
| 3       | 60 seconds          |
| 4       | 90 seconds          |
| 5       | 120 seconds         |

After 5 failed attempts, the webhook delivery is marked permanently failed. You can review failed deliveries and trigger manual redelivery in the **Axra dashboard → Webhooks → Delivery Logs**.

## Best practices

1. **Return `200` quickly** — acknowledge receipt immediately, then process the event in a background job. Long-running handlers risk timeouts and unnecessary retries.
2. **Handle duplicates** — network retries mean your endpoint may receive the same event more than once. Use `paymentId` as an idempotency key to ensure you process each event exactly once.
3. **Always verify signatures** — reject any request where `X-Axra-Signature` does not match your computed value.
4. **Use HTTPS in production** — plain HTTP webhook URLs are rejected for live-mode credentials.
5. **Use ngrok during local development** — run `ngrok http 3000` to get a public URL that tunnels to your local server.
