Webhook receiver (Node.js)
This guide shows a simple way to receive Coinset webhooks locally and test your endpoint from the public internet using a tunnel.
What you’ll build
- A single-file Node.js webhook receiver that:
- accepts Coinset deliveries (
POST /webhook) - handles verification (
coinset.webhook.verify) - verifies signatures (
X-Coinset-Signature) - logs delivery ids and event ids
- accepts Coinset deliveries (
- A tunnel (LocalTunnel, Cloudflare Tunnel, or ngrok) that gives you a public HTTPS URL.
1) Create a minimal receiver
Create a file named receiver.mjs:
import http from "node:http";
import crypto from "node:crypto";
const PORT = Number(process.env.PORT || 8787);
const SECRET = process.env.COINSET_WEBHOOK_SECRET || "";
function timingSafeEqualHex(a, b) {
const aa = Buffer.from(String(a || ""), "hex");
const bb = Buffer.from(String(b || ""), "hex");
return aa.length === bb.length && crypto.timingSafeEqual(aa, bb);
}
function verifySignature({ secret, timestampMs, rawBody, signatureHex }) {
if (!secret) return false;
const mac = crypto.createHmac("sha256", secret);
mac.update(`${timestampMs}.`);
mac.update(rawBody); // Buffer
const expected = mac.digest("hex");
return timingSafeEqualHex(expected, signatureHex);
}
const server = http.createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhook") {
res.writeHead(404);
res.end("not found");
return;
}
// Read raw body bytes (required for signature verification).
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
const webhookId = req.headers["x-coinset-webhook-id"];
const deliveryId = req.headers["x-coinset-delivery-id"];
const eventId = req.headers["x-coinset-event-id"];
const timestampMs = req.headers["x-coinset-timestamp-ms"];
const sigHex = req.headers["x-coinset-signature"];
// Verify signature (recommended).
if (SECRET) {
const ok = verifySignature({
secret: SECRET,
timestampMs,
rawBody,
signatureHex: sigHex,
});
if (!ok) {
res.writeHead(401, { "content-type": "text/plain" });
res.end("bad signature");
return;
}
}
let body;
try {
body = JSON.parse(rawBody.toString("utf8"));
} catch {
res.writeHead(400, { "content-type": "text/plain" });
res.end("bad json");
return;
}
console.log("coinset webhook", {
webhookId,
deliveryId,
eventId,
type: body?.type,
});
// Verification handshake: echo challenge (optional, but nice).
if (body?.type === "coinset.webhook.verify" && body?.challenge) {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, challenge: body.challenge }));
return;
}
// Normal event delivery.
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
});
server.listen(PORT, () => {
console.log(`listening on http://localhost:${PORT}/webhook`);
if (!SECRET)
console.log("NOTE: COINSET_WEBHOOK_SECRET is not set (signature verification disabled).");
});
Run it:
export COINSET_WEBHOOK_SECRET="paste-your-webhook-secret-here"
node receiver.mjs
2) Expose it with a tunnel
Coinset requires a publicly reachable HTTPS URL (except for localhost during local dev).
Option A: LocalTunnel (recommended)
LocalTunnel is free and quick for testing.
npx localtunnel --port 8787
It will print a public https://....loca.lt URL. Your webhook URL becomes:
https://<your-subdomain>.loca.lt/webhook
Option B: Cloudflare Tunnel
Install cloudflared, then run:
cloudflared tunnel --url http://localhost:8787
It will print a public https://...trycloudflare.com URL. Your webhook URL becomes:
https://<your-subdomain>.trycloudflare.com/webhook
Option C: ngrok
ngrok http 8787
Use the https://...ngrok-free.app/webhook URL.
3) Register the webhook in Coinset
In the Coinset Developer UI:
- Create a webhook pointing at your tunnel URL (ending in
/webhook). - Coinset will send a verification request.
- Once it shows as
active, click Test to verify end-to-end delivery.
Common gotchas
- You must verify the signature using the raw body bytes, not a re-serialized JSON object.
- Use
X-Coinset-Event-Idfor idempotency (deliveries are at-least-once). - Return 2xx quickly; do slow work async (queue / background job).