> ## Documentation Index
> Fetch the complete documentation index at: https://developer.klikit.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Node receiver (reference)

> Minimal Express server that verifies, dedupes, and acknowledges klikit webhook events.

A copy-paste-runnable webhook receiver in Node + Express. Around 80 lines.
Demonstrates the four things you have to get right: raw-body capture,
constant-time signature compare, idempotency on `x-klikit-event-id`, and
fast 2xx acknowledgement.

<Tip>
  The source also lives in the partner-api repo at
  [examples/webhook-receiver/node](https://github.com/klikit/partner-api/tree/develop/examples/webhook-receiver/node)
  so you can clone it directly.
</Tip>

## Run it

```bash theme={null}
npm install
KLIKIT_WEBHOOK_SECRET=<your secret> node server.js
```

Listens on `:8080` and exposes `POST /webhooks/klikit`.

## Source

```js server.js theme={null}
// Klikit webhook receiver — minimal reference (Node + Express).
const crypto = require("crypto");
const express = require("express");

const PORT = process.env.PORT || 8080;
const SECRET = process.env.KLIKIT_WEBHOOK_SECRET;
if (!SECRET) {
  console.error("KLIKIT_WEBHOOK_SECRET not set");
  process.exit(1);
}

const app = express();

// IMPORTANT: capture the raw body BEFORE JSON parsing. The HMAC is computed
// over the bytes klikit sent — any reformatting (key reordering, whitespace
// changes) will break verification.
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

// Replace with your own persistent store (Redis SETNX, Postgres unique index, etc).
const seenEventIDs = new Set();

app.post("/webhooks/klikit", (req, res) => {
  const sigHeader = req.get("x-klikit-signature");
  const eventID = req.get("x-klikit-event-id");
  const eventType = req.get("x-klikit-event-type");

  if (!sigHeader || !eventID || !eventType) {
    return res.status(400).json({ error: "missing klikit headers" });
  }

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(req.rawBody)
    .digest("hex");

  const ok =
    sigHeader.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sigHeader), Buffer.from(expected));

  if (!ok) return res.status(401).json({ error: "invalid signature" });

  // Idempotency — klikit retries on non-2xx, so the same event_id may arrive
  // more than once. Drop duplicates silently with a 2xx.
  if (seenEventIDs.has(eventID)) {
    return res.status(200).json({ status: "duplicate-ignored" });
  }
  seenEventIDs.add(eventID);

  // Acknowledge fast. Do the actual work asynchronously so klikit's delivery
  // timeout (10s) isn't tied to your downstream processing.
  res.status(200).json({ status: "received" });

  setImmediate(() => handleEvent(eventType, req.body));
});

function handleEvent(eventType, payload) {
  const { brand_id, branch_id, orders } = payload;
  console.log(`[${eventType}] brand=${brand_id} branch=${branch_id} orders=${orders?.length ?? 0}`);

  switch (eventType) {
    case "klikit.order.created.v2":      /* push to POS */          break;
    case "klikit.order.status.updated":  /* update order record */  break;
    case "klikit.order.cart.updated":    /* replace cart state */   break;
    default: console.warn(`unknown event type: ${eventType}`);
  }
}

app.listen(PORT, () => console.log(`klikit webhook receiver listening on :${PORT}`));
```

## Hardening for production

The reference above runs as-is, but two things need real-world replacements
before you put it in production:

* **`seenEventIDs` is in-memory.** Replace with Redis (`SET key NX EX 86400`)
  or a Postgres unique index. An in-memory set evaporates on every restart,
  letting duplicates back in.
* **`setImmediate` is a single-process queue.** For real throughput, push to
  a durable queue (SQS, Pub/Sub, RabbitMQ, BullMQ) and process from a
  separate worker so an OOM in your processor can't drop events.
