Contents

Webhooks

The calls Nucleo makes to your endpoints, and how carriers push tracking events into Nucleo.

Two directions

Webhooks in Nucleo go both ways:

  • Outbound — Nucleo calls your endpoint when something happens. Today: the CMS revalidate call, after a page or an entry is published.
  • Inbound — a carrier or a tracking service calls Nucleo when a parcel moves. Nucleo matches the event to the shipment, updates the order and notifies the shopper.

CMS revalidate

Set it up in Nucleo under CMS > your site > Settings: enter your Revalidate URL and a Revalidate secret. Right after every publish Nucleo sends a signed request:

POST /api/revalidate HTTP/1.1
Host: www.acme.example
Content-Type: application/json
User-Agent: Nucleo-Webhooks/1.0
Nucleo-Signature: t=1791115200,v1=5f2b6c0e9a8d4f7b1c3e2a6d9b0f8e7c6a5d4b3c2e1f0a9b8c7d6e5f4a3b2c1d
Nucleo-Webhook-Id: 3f6c1a52-8e0b-4d7a-9c2e-1b5d7f9a0c34
Nucleo-Webhook-Event: content.published
Nucleo-Webhook-Attempt: 1

{"id":"3f6c1a52-8e0b-4d7a-9c2e-1b5d7f9a0c34","event":"content.published","site":"acme-website","locale":"en","path":"/blog","published_at":"2026-10-04T12:00:00+00:00","secret":"whsec_example_123"}
  • A page published sends path: "/" (refresh the whole site); an entry published sends /<collection-slug>.
  • 5-second timeout, redirects not followed. Answer with any 2xx; the body is ignored.
  • Retries. Any other answer, or no answer in time, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours — at most 6 attempts, then Nucleo gives up. Every attempt carries the same id (also in Nucleo-Webhook-Id) and its number in Nucleo-Webhook-Attempt: refreshing the cache twice is harmless, but you can skip an id you have already handled.
  • The secret field in the body is deprecated and will be removed: verify the signature instead.

Verify the signature before doing anything. Nucleo-Signature is t=<unix time>,v1=<signature>, where the signature is the lowercase hex HMAC-SHA256 of <t>.<raw body> keyed with your Revalidate secret. Compute it on the exact bytes you received (before parsing the JSON), compare in constant time, and reject a t older than 5 minutes. A request without the header means no secret is set on the site: set one.

// Next.js route handler: app/api/revalidate/route.js
import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidatePath } from "next/cache";

const TOLERANCE_SECONDS = 300;

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

export async function POST(req) {
  const rawBody = await req.text();
  const signature = req.headers.get("nucleo-signature") ?? "";
  if (!verify(signature, rawBody, process.env.NUCLEO_REVALIDATE_SECRET ?? "")) {
    return new Response("Forbidden", { status: 403 });
  }
  const { path = "/" } = JSON.parse(rawBody);
  revalidatePath(path === "/" ? "/" : path, "layout");
  return Response.json({ revalidated: true });
}

Send tracking events to Nucleo

Generic carrier connections receive events at POST https://api-commerce.nucleoplatform.com/api/oms/v1/webhooks/tracking/{connection}. The connection id and its webhook secret are given to you by the merchant or by Nucleo support when the carrier is connected.

Sign the exact bytes you send: X-Nucleo-Signature is the lowercase hex HMAC-SHA256 of the raw body, keyed with the webhook secret.

import { createHmac } from "node:crypto";

const body = JSON.stringify({
  events: [
    {
      tracking_number: "1Z999AA10123456784",
      status: "out_for_delivery",
      description: "Out for delivery",
      occurred_at: "2026-10-06T07:12:00+02:00",
      location: "Milano",
    },
  ],
});
const signature = createHmac("sha256", process.env.NUCLEO_WEBHOOK_SECRET).update(body).digest("hex");

await fetch(`https://api-commerce.nucleoplatform.com/api/oms/v1/webhooks/tracking/${connectionId}`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Nucleo-Signature": signature },
  body,
});

Events for unknown tracking numbers are ignored without error, and an event identical to one already stored is skipped, so you can safely resend after a timeout. See the webhook reference for the statuses and the Qapla' and DHL formats.