# 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
* 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`:

```js
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:

```bash
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.

```bash
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:

```bash
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

```bash
ngrok http 8787
```

Use the `https://...ngrok-free.app/webhook` URL.

## 3) Register the webhook in Coinset

In the Coinset Developer UI:

1. Create a webhook pointing at your tunnel URL (ending in `/webhook`).
2. Coinset will send a verification request.
3. 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-Id` for **idempotency** (deliveries are at-least-once).
* Return **2xx quickly**; do slow work async (queue / background job).
