Skip to main content

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 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:
PUT /business/config
{
  "webhookUrl": "https://your-domain.com/webhooks/axra"
}
Your webhook URL must be publicly reachable. During local development, use a tool like ngrok to expose your local server.

Event types

Axra delivers the following events to your webhook endpoint:
EventDescription
payment.completedPayment captured successfully
payment.failedPayment processing failed
payment.settledFunds cleared and available for withdrawal
payment.refundedPayment has been refunded
payment.disputedA dispute (chargeback) has been opened
payment.dispute_wonDispute resolved in your favor
payment.dispute_lostDispute resolved against you
collection.createdA local-rails collection was created.
collection.processingFunds have been detected, settlement is in progress.
collection.completedFunds settled; merchant USDC wallet credited.
collection.failedCollection failed (e.g. amount mismatch, provider rejection).
collection.expiredVirtual account expired without payment.

Webhook payload structure

Every webhook request shares the same envelope format:
{
  "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.
Fires when a payment is captured successfully. Update your order status to reflect payment received.
{
  "paymentId": "bpay_01H...",
  "amount": "49.99",
  "currency": "USD"
}
Fires when payment processing fails. The reason field contains the failure code.
{
  "paymentId": "bpay_01H...",
  "reason": "card_declined"
}
Fires when funds have cleared the card networks and are credited to your merchant wallet (minus fees).
{
  "paymentId": "bpay_01H...",
  "amount": "49.99",
  "currency": "USD",
  "settledAt": "2026-03-22T10:00:00.000Z"
}
Fires when a refund is issued for a payment.
{
  "paymentId": "bpay_01H...",
  "amount": 49.99,
  "currency": "USD",
  "refundId": "re_1Abc..."
}
Fires when a cardholder opens a dispute (chargeback). The full charge amount is debited from your account immediately, plus a non-refundable dispute fee.
{
  "paymentId": "bpay_01H...",
  "disputeId": "dp_1Abc...",
  "disputeAmount": 49.99,
  "disputeFee": 5.82,
  "reason": "fraudulent"
}
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.
{
  "paymentId": "bpay_01H...",
  "won": true,
  "chargeAmount": 49.99
}
Fires immediately after a collection is created through the direct API or hosted checkout. data.object mirrors GET /business/collections/{id}.
{
  "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"
  }
}
Fires when inbound funds are detected and settlement is in progress.
{
  "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"
  }
}
Fires when the collection settles and your USDC wallet is credited. Fulfill the order on this event.
{
  "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"
  }
}
Fires when the provider reports a terminal failure (for example, amount mismatch or rejection).
{
  "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"
  }
}
Fires when deposit instructions expire before payment arrives.
{
  "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"
  }
}

Webhook request headers

Axra includes these headers on every webhook request:
HeaderDescription
Content-Typeapplication/json
X-Axra-SignatureHMAC-SHA256 signature of the raw request body
X-Axra-EventThe event type (e.g., payment.completed)

Signature verification

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.
To verify a webhook:
1

Get your webhook secret

Find your webhookSecret in the Axra dashboard under Settings → API Keys. Store it as an environment variable — never hardcode it.
2

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.
3

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.
4

Reject if signatures do not match

Return a 401 Unauthorized response immediately. Do not process the event.
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');
});
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.

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:
AttemptDelay after failure
1Immediate
230 seconds
360 seconds
490 seconds
5120 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.