Copy-paste ready code for webhook handling and order creation in your stack.
const express = require("express");
const crypto = require("crypto");
const fetch = require("node-fetch"); // node 18+: built-in fetch works too
const app = express();
// IMPORTANT: use express.raw() for the webhook route to preserve the raw body
// for HMAC verification. Use express.json() for all other routes.
app.use("/webhooks", express.raw({ type: "application/json" }));
app.use(express.json());
const API_KEY = process.env.CHAINPAY_API_KEY; // sk_live_...
const WEBHOOK_SECRET = process.env.CHAINPAY_WEBHOOK_SECRET; // whsec_...
// POST /create-payment — called from your frontend
app.post("/create-payment", async (req, res) => {
const { amount, userId, yourOrderId } = req.body;
const response = await fetch("https://chainpay.pro/api/v1/orders", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: amount,
currency: "USDT",
chain: "trc20",
externalId: yourOrderId, // your DB order ID
metadata: { userId }, // returned in webhook
}),
});
if (!response.ok) {
const err = await response.json();
return res.status(response.status).json({ error: err.message });
}
const order = await response.json();
// Redirect user to the hosted checkout page
res.json({
orderId: order.orderId,
checkoutUrl: order.checkoutUrl,
expiresAt: order.expiresAt,
});
});// POST /webhooks/chainpay
app.post("/webhooks/chainpay", (req, res) => {
const signature = req.headers["x-chainpay-signature"];
// req.body is a Buffer because we used express.raw() above
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
let isValid = false;
try {
isValid = crypto.timingSafeEqual(
Buffer.from(signature ?? "", "hex"),
Buffer.from(expected, "hex")
);
} catch {
// timingSafeEqual throws if buffers differ in length
isValid = false;
}
if (!isValid) {
console.warn("Webhook signature mismatch");
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString());
switch (payload.event) {
case "payment.completed":
console.log(
`[ChainPay] Order ${payload.orderId} completed.`,
`External: ${payload.externalId}, Net: ${payload.netAmount} ${payload.currency}`
);
// TODO: mark order as paid in your database
// TODO: trigger fulfilment (send email, unlock content, etc.)
break;
case "payment.expired":
console.log(`[ChainPay] Order ${payload.orderId} expired.`);
// TODO: mark order as expired, notify the user if needed
break;
default:
console.log(`[ChainPay] Unknown event: ${payload.event}`);
}
// Always respond 200 quickly — heavy processing should be async (queue / worker)
res.status(200).send("OK");
});
app.listen(3000, () => console.log("Server running on port 3000"));// GET /order-status/:id — proxy to ChainPay
app.get("/order-status/:id", async (req, res) => {
const response = await fetch(
`https://chainpay.pro/api/v1/orders/${req.params.id}`,
{ headers: { "Authorization": `Bearer ${API_KEY}` } }
);
const order = await response.json();
// order.status: "pending" | "confirming" | "completed" | "expired" | "failed"
res.json(order);
});Key points to remember
X-ChainPay-Signature header before processing any webhook.200 OK quickly; run heavy processing asynchronously to avoid timeouts.sk_test_ keys + POST /api/v1/sandbox/simulate-payment for end-to-end testing without real funds.