Xtopay

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 TypeDescription
payment.succeededDispatched when a payment transaction successfully changes to SUCCEEDED.
invoice.paidDispatched 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 with sha256= (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?

Edit this page on GitHub
Last updated on June 5, 2026

On this page