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

# Create a webhook

> Configure webhooks and receive notifications

Webhooks notify your server when payment or settlement events happen. Create and manage them from the dashboard without writing code.

## View your webhooks

Go to **Settings > Developers > Webhooks** to see your endpoints. Each row shows the name, endpoint URL, status, and last activity. Filter by **Active** or **Inactive** to narrow the list.

## Create a webhook

1. From **Settings > Developers > Webhooks**, select **Create webhook**.
2. Select one or more events that trigger the webhook. At least one is required. See Events for the full list.
3. Enter a name that identifies its purpose. Required.
4. Enter the endpoint URL where Lite sends event notifications. Required.
5. Add a description of what the webhook is for. Optional, up to 1,000 characters.
6. Add custom headers to send with every delivery. Optional. See Custom headers.

## Events

Select any combination of the following. Event names arrive in the `event` field of the payload.

| Event                        | Triggered when                   |
| ---------------------------- | -------------------------------- |
| `payment.authorized`         | Payment authorization succeeded. |
| `payment.captured`           | Full capture completed.          |
| `payment.partially_captured` | Partial capture completed.       |
| `payment.refunded`           | Refund processed.                |
| `payment.voided`             | Payment voided.                  |
| `payment.failed`             | Payment failed.                  |

Registration is validated against this list. An unrecognized event name is rejected with a `400 Bad Request` that lists the invalid entries and the allowed values.

## Custom headers

Custom headers are sent with every delivery to your endpoint. Use them to pass values your server expects, such as an authentication token or a routing key.

Each header is a name and value pair. Select **Add Header** to add more than one.

## Verify signatures

lite signs every delivery with HMAC-SHA256 so you can confirm it came from lite and was not tampered with. Verify the signature on your server before you trust the body. On its own, treat a webhook as a trigger to read the payment server-side, never as proof that money moved.

lite uses a signing secret tied to your webhook. Store it server-side and never expose it in client code.

Every delivery includes these headers, plus any custom headers you configured:

| Header              | Value                                                   |
| ------------------- | ------------------------------------------------------- |
| `Content-Type`      | `application/json`                                      |
| `X-Lite-Signature`  | HMAC-SHA256 of the raw request body, lowercase hex.     |
| `X-Webhook-Version` | Signature scheme version. Currently `1`.                |
| `X-Timestamp`       | Delivery time, ISO 8601 (UTC).                          |
| `X-Idempotency-Key` | Stable id for the event. Use it to deduplicate retries. |
| `X-Request-Mode`    | `live` or `sandbox`.                                    |

To verify: recompute `HMAC-SHA256(secret, rawBody)`, hex-encode it, and compare it to `X-Lite-Signature` with a constant-time comparison.

<Warning>
  Hash the raw request body exactly as received. Read the raw bytes before any JSON parsing, because parsing and re-serializing can change the bytes and break the match.
</Warning>

```js theme={null} theme={null}
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// express.raw gives you the untouched body as a Buffer
app.post('/webhooks/lite', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('X-Lite-Signature');
  const rawBody = req.body; // Buffer

  const expected = crypto
    .createHmac('sha256', process.env.LITE_WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');

  const valid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex'),
  );
  if (!valid) return res.status(401).json({ error: 'invalid signature' });

  const event = JSON.parse(rawBody.toString('utf8'));
  res.status(200).json({ received: true });
  // process event...
});
```

```python theme={null} theme={null}
import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)

def verify(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.post("/webhooks/lite")
def handle_webhook():
    signature = request.headers.get("X-Lite-Signature", "")
    payload = request.get_data()  # raw body bytes
    if not verify(payload, signature, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # process event...
    return {"received": True}, 200
```

Always compare with a constant-time function (`crypto.timingSafeEqual`, `hmac.compare_digest`, or the equivalent in your language). A plain `==` comparison can leak the signature through timing.

## Event payload

Every delivery uses one envelope: `{ event, mode, data }`. `event` is the event type from the list above. `mode` is `live` or `sandbox`. `data` carries the entity. Match the payment using `data.orderId` (your order reference) or `data.id`.

Return `200` as soon as you receive a delivery, then process it. Deduplicate on `X-Idempotency-Key`, since a delivery can arrive more than once. You can also reject deliveries whose `X-Timestamp` is older than a few minutes to limit replay.

```json theme={null} theme={null}
{
  "event": "payment.captured",
  "mode": "live",
  "data": {
    "id": "b21dc531-12c7-41cd-bd1a-e5986cf6a28e",
    "orderId": "ORD-123456",
    "amount": 1100,
    "currency": "SAR",
    "status": "CAPTURED",
    "paymentMethod": "CARD",
    "channelId": "1ddbfa13-8926-45cb-9ebc-8b2196bcc9a4"
  }
}
```

If your endpoint does not return `200`, lite retries with exponential backoff, starting around 1 minute and up to 12 attempts, which spans roughly 3 days before delivery is abandoned.
