Webhooks Integration
Securely receive and verify real-time event notifications from Xtopay.
Webhooks Integration
Webhooks allow you to receive real-time, event-driven HTTP notifications from Xtopay when actions occur in your account (such as payment checkouts succeeding or invoices being paid).
This guide explains how webhooks are signed, how to verify them, and lists the payload structures.
Event Types
Xtopay dispatches these core webhook events:
| Event Type | Description |
|---|---|
payment.succeeded | Dispatched when a payment transaction successfully changes to SUCCEEDED. |
invoice.paid | Dispatched when a pending subscription or manual invoice is fully paid. |
Webhook Payload Structure
All webhooks are sent as HTTP POST requests containing a JSON body:
payment.succeeded
{
"id": "evt_cl8z2n...",
"event": "payment.succeeded",
"environment": "TEST",
"createdAt": "2026-06-05T12:00:05Z",
"data": {
"id": "pay_cl8z2e8z20000g...",
"reference": "cl8z2e8z20000g...",
"status": "SUCCEEDED",
"amount": 5000,
"currency": "GHS",
"fee": 75,
"net": 4925,
"description": "Premium Subscription Plan Purchase",
"paidAt": "2026-06-05T12:00:04Z",
"metadata": {
"orderId": "order_abc123"
},
"customer": {
"id": "cust_cl8z2e9z...",
"name": "Jane Doe",
"email": "jane.doe@example.com"
}
}
}invoice.paid
{
"id": "evt_cl8z2o...",
"event": "invoice.paid",
"environment": "TEST",
"createdAt": "2026-06-05T12:00:05Z",
"data": {
"id": "inv_cl8z2p...",
"paymentId": "pay_cl8z2e8z20000g...",
"amount": 5000,
"currency": "GHS"
}
}Verifying Webhook Signatures
To ensure webhook events were dispatched by Xtopay and not tampered with, you must verify the signature header.
Every webhook request contains these HTTP headers:
x-xtopay-signature: The computed HMAC signature, prefixed withsha256=(e.g.sha256=a88b...).x-xtopay-event-id: The unique event payload identifier.
Compute and Compare
Compute the HMAC-SHA256 signature using the raw JSON request body and your endpoint's unique signing secret (retrieved from the dashboard developer webhook tab). Compare your computed signature against the signature header.
[!IMPORTANT] You must compute the HMAC signature using the raw request body string (raw bytes). Do not parse the body to an object first, as JSON formatting differences will cause signature mismatches.
Node.js Verification Example
Here is a copy-pasteable example verifying Xtopay webhooks using Express:
import express from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = "whsec_your_webhook_endpoint_secret";
// Use express.raw() to preserve the exact raw body string
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signatureHeader = req.headers["x-xtopay-signature"] as string;
if (!signatureHeader || !signatureHeader.startsWith("sha256=")) {
return res.status(400).send("Missing or malformed signature header.");
}
// Extract the hex signature
const signature = signatureHeader.replace("sha256=", "");
const rawBody = req.body.toString("utf8");
// Compute the expected HMAC signature
const expectedSignature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
// Constant-time comparison to prevent timing attacks
const isSignatureValid = crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex")
);
if (!isSignatureValid) {
console.error("Webhook signature verification failed.");
return res.status(401).send("Invalid signature.");
}
// Parse payload once verified
const event = JSON.parse(rawBody);
console.log(`Received event: ${event.event} [${event.id}]`);
// Acknowledge receipt of the webhook with a 2xx status code
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log("Server listening on port 3000"));How is this guide?