Contents

Warehouse webhooks

The calls Nucleo makes to your warehouse system when an order is ready, cancelled or a return is expected.

When the merchant sets your webhook URL on the connection, Nucleo calls it — so you can poll less often. Three events:

EventWhenThen
order.readyAn order assigned to your warehouse is ready.GET /orders/{number}, then accept it.
order.cancelledAn order that was ready is cancelled. data.status is cancelled, or cancel_requested if you had accepted it.Stop it in your WMS.
return.expectedA return to your warehouse is approved.GET /returns/{id}, then confirm it.

Webhooks are hints, not data: the body carries only the IDs, and you read the current state with the API. Keep polling as a fallback (for example every 15 minutes): an order can also become ready later than its event, and a webhook can fail.

POST /hooks/nucleo HTTP/1.1
Host: wms.acme.example
Content-Type: application/json
User-Agent: Nucleo-Webhooks/1.0
X-Nucleo-Signature: t=1791880191,v1=5f2b6c0e9a8d4f7b1c3e2a6d9b0f8e7c6a5d4b3c2e1f0a9b8c7d6e5f4a3b2c1d
X-Nucleo-Event: order.ready
X-Nucleo-Webhook-Id: 5b7e1c2d-3a4f-4b6c-8d9e-0f1a2b3c4d5e
X-Nucleo-Attempt: 1

{"id":"5b7e1c2d-3a4f-4b6c-8d9e-0f1a2b3c4d5e","type":"order.ready","created_at":"2026-10-13T08:29:51Z","data":{"order_id":"3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b","order_number":"#1042"}}

Verify the signature before doing anything. X-Nucleo-Signature is t=<unix time>,v1=<signature>, where the signature is the lowercase hex HMAC-SHA256 of <t>.<raw body> keyed with the webhook secret (whsec_…). Compute it on the exact bytes you received, compare in constant time, and reject a t more than 5 minutes away from your clock.

import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();

function verify(header, rawBody, secret) {
  const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

app.post("/hooks/nucleo", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(req.get("X-Nucleo-Signature"), raw, process.env.NUCLEO_WEBHOOK_SECRET)) return res.sendStatus(403);
  const event = JSON.parse(raw);
  if (await alreadyHandled(event.id)) return res.sendStatus(200);
  await queue.push(event); // answer fast, work later
  res.sendStatus(200);
});
  • Answer with any 2xx within 10 seconds; redirects are not followed. Anything else is retried with exponential backoff — 8 attempts in about an hour — then Nucleo gives up and alerts the merchant.
  • Every attempt carries the same id (also in X-Nucleo-Webhook-Id) and its number in X-Nucleo-Attempt: skip an id you have already handled.
  • While the connection is paused or still a draft, no webhook is sent, and the events of that time are not sent later: catch up by polling.
  • The webhook URL must be https and reachable from the internet.