// Secure server-to-server integration example — NO WordPress, NO plugins. // A merchant's backend talks to the gateway directly and verifies signed webhooks. // Run: GATEWAY_URL=http://localhost:3000 API_KEY=pk_... API_SECRET=sk_... node examples/merchant-backend.mjs import { createServer } from "node:http"; import { createHmac, timingSafeEqual } from "node:crypto"; const GATEWAY = process.env.GATEWAY_URL ?? "http://localhost:3000"; const API_KEY = process.env.API_KEY ?? ""; const API_SECRET = process.env.API_SECRET ?? ""; const PORT = Number(process.env.PORT ?? 4000); const seenWebhooks = new Set(); // idempotency by X-Webhook-Id const fulfilled = new Set(); function readBody(req) { return new Promise((resolve) => { let b = ""; req.on("data", (c) => (b += c)); req.on("end", () => resolve(b)); }); } const server = createServer(async (req, res) => { // 1) customer hits checkout -> we create an invoice and redirect to hosted page if (req.method === "POST" && req.url === "/checkout") { const r = await fetch(`${GATEWAY}/v1/invoices`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, "Idempotency-Key": "order-" + Date.now(), // retry-safe }, body: JSON.stringify({ network: "ton", asset: "USDT", amount: "25.00", order_id: "order-" + Date.now(), }), }); const inv = await r.json(); // send the buyer to the gateway's hosted payment page (QR + live status) res.writeHead(302, { Location: `${GATEWAY}/pay/${inv.id}` }); return res.end(); } // 2) gateway calls our webhook -> verify signature, freshness, idempotency if (req.method === "POST" && req.url === "/crypto-webhook") { const raw = await readBody(req); const sig = req.headers["x-signature"] ?? ""; const expected = createHmac("sha256", API_SECRET).update(raw).digest("hex"); // constant-time compare const a = Buffer.from(String(sig)); const b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) { res.writeHead(401); return res.end("bad signature"); } const evt = JSON.parse(raw); // replay guard: reject stale events (> 5 min) if (Date.now() - new Date(evt.timestamp).getTime() > 5 * 60_000) { res.writeHead(400); return res.end("stale"); } // idempotency: process each webhook id once const wid = req.headers["x-webhook-id"]; if (wid && seenWebhooks.has(wid)) { res.writeHead(200); return res.end("dup-ok"); } if (wid) seenWebhooks.add(wid); if ((evt.event === "invoice.paid" || evt.event === "invoice.overpaid") && !fulfilled.has(evt.order_id)) { fulfilled.add(evt.order_id); console.log(`✅ FULFILL order ${evt.order_id}: ${evt.amount_received} ${evt.asset}`); // …deliver goods / grant access here… } res.writeHead(200); return res.end("ok"); } res.writeHead(404); res.end("not found"); }); server.listen(PORT, () => console.log(`merchant backend on :${PORT} (gateway=${GATEWAY})\n` + ` POST /checkout -> creates invoice, 302 to hosted pay page\n` + ` POST /crypto-webhook -> verifies HMAC + freshness + idempotency`), );