Skip to main content
The platform sends a signed HTTP POST to your callback_url whenever a payment changes state.

Payload

{
  "event": "payment.completed",
  "payment_intent_id": "pi_xxxx",
  "merchant_id": "a1b2c3d4-...",
  "amount": 29.99,
  "currency": "USD",
  "status": "completed",
  "completed_at": "2025-01-01T12:31:55Z",
  "reference": "ORDER-1042",
  "metadata": { "table": "5", "cashier": "Maria" }
}

Webhook Events

EventTrigger
payment.completedCustomer payment confirmed
payment.expiredQR or approval window elapsed without payment
payment.refundedMerchant issued a refund

Verifying the Signature

Every webhook delivery includes an X-Webhook-Signature header — an HMAC-SHA256 hex digest of the raw request body, signed with your counter’s webhook_secret.
Always use constant-time comparison (hmac.compare_digest / timingSafeEqual) to prevent timing attacks. Never use a simple string equality check.
Python:
import hmac, hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
Node.js:
const crypto = require('crypto');

function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Acknowledging Delivery

Respond with HTTP 200 to acknowledge receipt. Deliveries returning a non-2xx status are retried up to 5 times with exponential backoff.