QuataPay
Developer docs

Webhooks

Receive real-time signed events when payments complete or fail.

Browse developer docs
Back to developer docs

Webhooks

QuataPay sends signed HTTP POST requests to your configured endpoint when payment events occur.


Configure an endpoint

In your merchant dashboard go to Developer → Webhooks and add your HTTPS endpoint URL. Only HTTPS endpoints are accepted.


Signature verification

Every webhook request includes an X-QuataPay-Signature header:

X-QuataPay-Signature: sha256=abc123…

The value is sha256= followed by the HMAC-SHA256 of the raw request body, keyed with your webhook secret. Always verify this before processing.

Node.js

const crypto = require("crypto");

function verifySignature(secret, rawBody, header) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(header),
  );
}

Python

import hashlib, hmac

def verify_signature(secret: str, raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)

PHP

function verifySignature(string $secret, string $rawBody, string $header): bool {
    $expected = "sha256=" . hash_hmac("sha256", $rawBody, $secret);
    return hash_equals($expected, $header);
}

Events

payment.succeeded

Fired when a customer successfully pays a checkout intent.

{
  "event": "payment.succeeded",
  "data": {
    "id": "pay_abc",
    "slug": "abc123def456",
    "status": "succeeded",
    "amount": 5000,
    "currency": "XAF",
    "description": "Order #1042",
    "customer_reference": "cust_abc123",
    "transaction_id": "tx_xyz",
    "metadata": {},
    "created_at": "2025-05-11T09:00:00Z",
    "completed_at": "2025-05-11T09:01:34Z"
  }
}

payment.failed

Fired when a payment attempt fails (insufficient funds, PIN error, etc.).

{
  "event": "payment.failed",
  "data": {
    "id": "pay_abc",
    "slug": "abc123def456",
    "status": "failed",
    "failure_reason": "INSUFFICIENT_FUNDS",
    …
  }
}

payment.cancelled

Fired when a customer cancels or the intent TTL expires.

{
  "event": "payment.cancelled",
  "data": {
    "id": "pay_abc",
    "status": "cancelled",
    …
  }
}

Retry policy

If your endpoint does not return 2xx within 15 seconds, QuataPay retries with exponential back-off:

AttemptDelay
21 min
310 min
41 hour
56 hours

After 5 failed attempts the delivery is marked failed. You can view and retry deliveries in Developer → Webhooks → Delivery log.


Best practices

  • Respond quickly (2xx) and process asynchronously — do not block on business logic.
  • Always verify the signature before processing.
  • Make your handler idempotent — the same event may be delivered more than once.
  • Use the id field, not the slug, as a stable unique identifier for the payment.