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

# Custom Webhooks

> Receive real-time HTTP callbacks for payment events

Build custom integrations with any system by receiving real-time webhook notifications for payment events.

## Overview

Whenever a payment event occurs (paid, expired, cancelled, refunded), Quickei sends an HTTP POST request to your configured `callback_url` with a JSON payload. The request includes an HMAC-SHA256 signature for verification.

## Webhook Flow

```mermaid theme={null}
sequenceDiagram
    participant Your System
    participant Quickei

    Your System->>Quickei: POST /orders (callback_url)
    Quickei-->>Your System: 200 OK (order created)
    Note over Quickei: Customer pays...
    Quickei->>Your System: POST callback_url (signed payload)
    Your System->>Your System: Verify signature
    Your System-->>Quickei: 200 OK
    Note over Quickei: If no 200, retry with backoff
```

## Events

| Event                 | Trigger                      |
| --------------------- | ---------------------------- |
| `pos.order.paid`      | Customer completed payment   |
| `pos.order.expired`   | Order expired before payment |
| `pos.order.cancelled` | Order cancelled by merchant  |
| `pos.order.refunded`  | Payment refunded to customer |

## Payload Format

```json theme={null}
{
  "event": "pos.order.paid",
  "timestamp": "2026-03-15T14:02:30+00:00",
  "data": {
    "order_id": "POS-20260315-A1B2C3",
    "amount": 25.00,
    "currency": "EUR",
    "status": "PAID",
    "reference": "INV-2026-001",
    "terminal_id": "terminal-01",
    "trx_id": "TRX-98765",
    "paid_at": "2026-03-15T14:02:30+00:00"
  }
}
```

## Headers

| Header                | Description                               |
| --------------------- | ----------------------------------------- |
| `Content-Type`        | `application/json`                        |
| `X-Quickei-Event`     | Event name (e.g. `pos.order.paid`)        |
| `X-Quickei-Signature` | HMAC-SHA256 signature of the request body |

## Signature Verification

Every webhook includes an `X-Quickei-Signature` header. **Always verify** the signature before processing the event.

<Tabs>
  <Tab title="PHP">
    ```php theme={null}
    $payload = file_get_contents('php://input');
    $signature = $_SERVER['HTTP_X_QUICKEI_SIGNATURE'] ?? '';

    $expected = 'sha256=' . hash_hmac(
        'sha256',
        $payload,
        $client_secret  // Your API client secret
    );

    if (!hash_equals($expected, $signature)) {
        http_response_code(403);
        exit('Invalid signature');
    }

    // Signature valid — process the event
    $event = json_decode($payload, true);

    switch ($event['event']) {
        case 'pos.order.paid':
            // Update order, send confirmation email, etc.
            break;
        case 'pos.order.refunded':
            // Process refund in your system
            break;
    }

    http_response_code(200);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib
    import json

    def handle_webhook(request):
        payload = request.body
        signature = request.headers.get('X-Quickei-Signature', '')

        expected = 'sha256=' + hmac.new(
            client_secret.encode(),
            payload,
            hashlib.sha256
        ).hexdigest()

        if not hmac.compare_digest(expected, signature):
            return HttpResponse(status=403)

        event = json.loads(payload)

        if event['event'] == 'pos.order.paid':
            # Update order in your system
            pass

        return HttpResponse(status=200)
    ```
  </Tab>

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

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

      const expected = 'sha256=' + crypto
        .createHmac('sha256', clientSecret)
        .update(payload)
        .digest('hex');

      if (signature !== expected) {
        return res.status(403).send('Invalid signature');
      }

      // Signature valid — process the event
      const { event, data } = req.body;

      if (event === 'pos.order.paid') {
        // Update order in your system
      }

      res.sendStatus(200);
    });
    ```
  </Tab>
</Tabs>

## Retry Policy

If your endpoint does not return a `200` status code, Quickei retries with **exponential backoff**:

| Attempt   | Delay       | Total Wait |
| --------- | ----------- | ---------- |
| 1st retry | 10 seconds  | 10s        |
| 2nd retry | 60 seconds  | 1m 10s     |
| 3rd retry | 300 seconds | 6m 10s     |

After 3 failed attempts, the webhook is marked as **failed**. Check the **API Logs** in your Merchant Dashboard for delivery status.

## Best Practices

<CardGroup cols={2}>
  <Card title="Always verify signatures" icon="shield-check">
    Never process a webhook without verifying the `X-Quickei-Signature` header. This prevents spoofed events.
  </Card>

  <Card title="Return 200 quickly" icon="bolt">
    Acknowledge the webhook immediately with a `200` response. Do heavy processing (email, inventory, etc.) asynchronously.
  </Card>

  <Card title="Handle duplicates" icon="clone">
    Webhooks may be delivered more than once. Use the `order_id` as an idempotency key to prevent double-processing.
  </Card>

  <Card title="Use HTTPS only" icon="lock">
    Webhook URLs must use HTTPS. HTTP endpoints and private/localhost IPs are rejected.
  </Card>
</CardGroup>

<Warning>
  Callback URLs pointing to `localhost`, `127.0.0.1`, or private IP ranges (10.x, 172.16-31.x, 192.168.x) are blocked for security.
</Warning>
