Skip to content

Webhooks

Webhooks push changes to your systems as they happen: we POST a signed JSON event to your HTTPS endpoint for every subscribed event.

GET/webhooks
Endpoints with delivery stats
POST/webhooks
Register an endpoint (secret shown once)
PATCH/webhooks/{id}
Change events or URL, pause or resume
DELETE/webhooks/{id}
Remove an endpoint

Endpoints can also be managed under Developers → Webhooks, which shows every delivery and lets you resend one. Through the API the key needs the webhooks:write scope.

cURL
bash
curl -X POST "https://library.retailcommerceai.com/api/v1/webhooks" \
  -H "Authorization: Bearer cl_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://erp.example.com/hooks/catalog",
  "events": [
    "product.updated",
    "product.deleted",
    "export.completed"
  ],
  "description": "ERP catalog sync"
}'

The response contains secret (whsec_…) once — store it; later responses only show a masked preview. Subscribe to "*" for every event.

Events

Eventdata
product.created{ product } — the new CanonicalProduct
product.updated{ product, changedFields } — after any change
product.deleted{ id, sku }
product.status_changed{ product, changedFields, previousStatus }
category.createdthe category
category.updatedthe category
category.deleted{ id, code, name }
attribute.createdthe attribute definition
attribute.updatedthe attribute definition
attribute.deleted{ id, code, label }
asset.createdthe asset
asset.deletedthe asset
import.completed{ jobId, type, status, result }
export.completed{ jobId, type, status, result } — result.fileKey is the file
channel.publish.completed{ jobId, type, status, result }
job.failed{ jobId, type, status: "failed", result: { error } }

Request

What your endpoint receives
http
POST /hooks/catalog HTTP/1.1
Content-Type: application/json
X-CL-Event: product.updated
X-CL-Delivery: cm1dlv…
X-CL-Timestamp: 1767225600
X-CL-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_4f2GkQm9cX5fGa1b9KpZ7Qw2",
  "type": "product.updated",
  "createdAt": "2026-09-24T08:12:45.120Z",
  "workspaceId": "cm1w0rk…",
  "data": {
    "product": {
      "id": "cm1…",
      "sku": "FR-OUD-100",
      "version": 8,
      "…": "…"
    },
    "changedFields": [
      "price",
      "stock"
    ]
  }
}

Verify the signature

  1. Split X-CL-Signature on commas into t (Unix seconds) and one or more v1 values.
  2. Compute HMAC-SHA256 with your endpoint secret over <t>.<raw request body> and hex-encode it.
  3. Accept the request if it equals any v1 value (constant-time compare) and t is within 5 minutes of your clock.

Always verify against the raw bytes you received — re-serialized JSON will not match.

Node.js
javascript
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.CL_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;

function verifySignature(rawBody, header, secret) {
  let timestamp = null;
  const signatures = [];
  for (const part of (header ?? "").split(",")) {
    const [key, value] = part.trim().split("=", 2);
    if (key === "t") timestamp = Number(value);
    if (key === "v1") signatures.push(value);
  }
  if (!timestamp || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
  return signatures.some(
    (sig) => /^[0-9a-f]{64}$/i.test(sig) && crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex")),
  );
}

const app = express();
// the signature covers the exact bytes: read the raw body, parse JSON only after verifying
app.post("/webhooks/content-library", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifySignature(req.body, req.get("X-CL-Signature"), SECRET)) return res.status(400).send("bad signature");
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200); // answer fast, then work
  queue.add(event); // de-duplicate on event.id
});

Delivery and retries

  • Respond with any 2xx within 10 seconds. Do slow work after answering (queue it).
  • Other answers and timeouts are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours.
  • After 20 consecutive failed deliveries the endpoint is switched off; turn it back on in the app or with PATCH active: true.
  • Events can arrive more than once and out of order: de-duplicate on the event id, and compare data.product.version or re-fetch the product when order matters.
  • Redirects are not followed; use the final HTTPS URL.

Test your endpoint

The app's Developers → Webhooks page can send a signed webhook.test event to your endpoint and shows the response.