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

# Webhooks

> Webhook events, signatures, and delivery behavior.

The platform sends a signed HTTP POST to your `callback_url` whenever a payment changes state.

### Payload

```json theme={null}
{
  "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

| Event               | Trigger                                       |
| ------------------- | --------------------------------------------- |
| `payment.completed` | Customer payment confirmed                    |
| `payment.expired`   | QR or approval window elapsed without payment |
| `payment.refunded`  | Merchant 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:**

```python theme={null}
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:**

```javascript theme={null}
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.

***
