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

# Webhook Notifications — XPayLabs Event Callbacks Reference

> XPayLabs delivers signed webhook callbacks to your configured endpoint URL when order or collection status changes. Learn how to configure endpoints and verify payloads.

XPayLabs sends HTTP POST callbacks to your configured endpoint URL whenever an order or collection changes state. Instead of polling the API for status changes, register your callback URL and receive signed notifications in real-time.

## Configuration

Set your webhook endpoint URL and shared secret in the gateway configuration:

```yaml theme={null}
xpay:
  merchant:
    callback-url: "https://yourapp.com/webhooks/xpaylabs"
    webhook-secret: "your-webhook-secret-here"
```

The `webhook-secret` is used to compute the `sign` field in each webhook payload, allowing you to verify the notification originated from your gateway.

## Payload Format

Every webhook callback is a `POST` request with a JSON body following the `NotifyPayload` structure:

| Field        | Type    | Description                                                        |
| ------------ | ------- | ------------------------------------------------------------------ |
| `sign`       | string  | HMAC-SHA256 signature for payload verification                     |
| `timestamp`  | integer | Unix timestamp of when the notification was generated              |
| `nonce`      | string  | Unique event identifier                                            |
| `notifyType` | string  | The event type (see [Event Types](/api-reference/webhooks/events)) |
| `data`       | object  | The event payload (NotifyOrder object)                             |

### Example Payload

```json theme={null}
{
  "sign": "a1b2c3d4e5f6...",
  "timestamp": 1717000123,
  "nonce": "550e8400-e29b-41d4-a716-446655440000",
  "notifyType": "ORDER_SUCCESS",
  "data": {
    "orderId": "order_1042",
    "uid": "user_42",
    "orderType": "COLLECTION",
    "status": "SUCCESS",
    "reason": null,
    "amount": "250.00",
    "actualAmount": "249.50",
    "fee": "0.00",
    "transaction": {
      "chain": "TRON",
      "symbol": "USDT",
      "txid": "a1b2c3d4e5f6...",
      "from": "TXyz...",
      "to": "TWkKZkmuB8DpVeiMoHiKf99ZoFHzk73CqR",
      "amount": "250.00",
      "blockNum": 12345678,
      "confirmedNum": 3,
      "status": "SUCCESS",
      "timestamp": 1717000123
    }
  }
}
```

## Signature Verification

Verify the webhook signature using your `webhook-secret`:

### Algorithm

1. Serialize the `data` object (the `data` field from the payload) as compact JSON.
2. Compute `HMAC-SHA256(data_json, webhook_secret)`.
3. Convert to lowercase hex and compare with the `sign` field.

### Node.js

```javascript theme={null}
import crypto from "crypto";

export function verifyWebhook(payload, secret) {
  const dataJson = JSON.stringify(payload.data);  // compact JSON
  const expectedSign = crypto
    .createHmac("sha256", secret)
    .update(dataJson, "utf8")
    .digest("hex");

  if (!crypto.timingSafeEqual(
    Buffer.from(expectedSign, "hex"),
    Buffer.from(payload.sign, "hex")
  )) {
    throw new Error("Invalid webhook signature");
  }

  return true;
}

// Express.js example
app.post("/webhooks/xpaylabs", express.json(), (req, res) => {
  try {
    verifyWebhook(req.body, process.env.XPAYLABS_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send("Verification failed");
  }

  const event = req.body;
  // Process event...
  res.status(200).send("ok");
});
```

### Python

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

app = Flask(__name__)
WEBHOOK_SECRET = "your-webhook-secret"

@app.route("/webhooks/xpaylabs", methods=["POST"])
def handle_webhook():
    payload = request.get_json()
    data_json = json.dumps(payload["data"], separators=(",", ":"))
    expected_sign = hmac.new(
        WEBHOOK_SECRET.encode(),
        data_json.encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected_sign, payload["sign"]):
        return "Invalid signature", 400

    notify_type = payload["notifyType"]
    # Process event...
    return "", 200
```

## Retry Policy

If your endpoint does not return a `2xx` status code within 10 seconds, XPayLabs retries the webhook with exponential backoff:

| Attempt   | Delay        |
| --------- | ------------ |
| 1st retry | \~1 second   |
| 2nd retry | \~5 seconds  |
| 3rd retry | \~30 seconds |
| 4th retry | \~5 minutes  |

After 4 failed attempts, the notification is marked as failed. Failed webhooks are logged and can be replayed from the gateway dashboard.

## Best Practices

* **Respond immediately.** Return `200` as fast as possible, then process the event asynchronously.
* **Verify every payload.** Always check the HMAC signature before acting on a webhook.
* **Use `nonce` for deduplication.** Store processed nonces to handle at-least-once delivery.
* **Check `notifyType`.** Route events based on the notification type to handle each event correctly.
