Webhooks
Instead of polling, register an HTTPS endpoint and we will POST a signed JSON event to it whenever something happens on your account. Manage your endpoints and signing secrets from the Webhooks page in your dashboard — the secret is shown only once when you create an endpoint.
POST
https://your-app.com/webhooks/payoutsEach event is delivered as an HTTP POST with a JSON body to every enabled endpoint you have registered. Respond with any 2xx status to acknowledge receipt.
Receiver endpoint examples
express
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.WEBHOOK_SECRET; // whsec_...
function verify(rawBody, header) {
if (!header) return false;
const { t, v1 } = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
if (!t || !v1) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
// timingSafeEqual throws on length mismatch — guard first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw keeps the exact bytes that were signed (express.json would not).
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const sig = req.get("X-Webhook-Signature");
if (!verify(raw, sig)) return res.status(401).send("invalid signature");
const event = JSON.parse(raw); // { id, type, createdAt, data }
// req.get("X-Webhook-Id") is stable across retries — use it to deduplicate.
switch (event.type) {
case "end_user.kyc.updated":
console.log("KYC:", event.data.endUser);
break;
case "payment.status.updated":
console.log("Payment:", event.data.payment);
break;
}
res.status(200).json({ received: true }); // ack fast, do heavy work async
});
app.listen(3000);