Gareth Wilson Gareth Wilson

Guide to Commerce Layer Webhooks: Features and Best Practices

Published


Commerce Layer webhooks notify your systems when commerce events happen: an order is placed, an order is approved, a return is requested. If you're building fulfillment, notifications, or downstream automation on Commerce Layer, webhooks are how you react in real time without polling.

This guide covers how Commerce Layer webhooks work, the topic model, how to verify the X-CommerceLayer-Signature, the circuit breaker that disables failing webhooks, and the best practices for production.

What are Commerce Layer webhooks?

Commerce Layer webhooks are HTTP POSTs delivered to a callback_url you register, each carrying a JSON:API resource matching the REST API format. You create a webhook with a topic ({resource}.{trigger}), and the create response returns a shared_secret used to sign callbacks.

Commerce Layer webhook features

FeatureDetails
ConfigurationPOST /api/webhooks (topic, callback_url, optional include_resources)
Signature headerX-CommerceLayer-Signature
Signature schemebase64 HMAC-SHA256 over the raw body, keyed with the webhook's shared_secret
Topic format{resource}.{trigger} (e.g. orders.place)
PayloadJSON:API resource (same shape as the REST API)
AcknowledgementReturn 2xx within 5 seconds
Circuit breakerUp to 10 retries; owner notified after 5 failures; disabled after 30 consecutive failures
State fieldscircuit_state, circuit_failure_count
SDK@commercelayer/sdk (official JS)

Common topics

Topics combine a resource and a trigger as {resource}.{trigger}:

TopicFires when
orders.placeAn order is placed
orders.approveAn order is approved
returns.requestA return is requested

Many {resource}.{trigger} combinations exist; subscribe to the topics you process, and use include_resources to have related resources sideloaded into the payload.

Setting up Commerce Layer webhooks

Create a webhook via POST /api/webhooks with a topic, a callback_url, and optionally include_resources:

curl -X POST "https://your-org.commercelayer.io/api/webhooks" \
  -H "Authorization: Bearer $CL_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhooks",
      "attributes": {
        "topic": "orders.place",
        "callback_url": "https://your-app.example.com/webhook",
        "include_resources": ["line_items"]
      }
    }
  }'

The create response returns a shared_secret; store it to verify deliveries.

Securing Commerce Layer webhooks

Each callback carries an X-CommerceLayer-Signature header: a base64 HMAC-SHA256 of the raw request body, keyed with that webhook's shared_secret. Commerce Layer's docs stress verifying against the raw, unparsed body.

const crypto = require("crypto");

const SHARED_SECRET = process.env.CL_WEBHOOK_SHARED_SECRET;

function verify(rawBody, signature) {
  const expected = crypto
    .createHmac("sha256", SHARED_SECRET)
    .update(rawBody)
    .digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-commercelayer-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge within 5s
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

The same check in Python:

import base64
import hashlib
import hmac
import os

SHARED_SECRET = os.environ["CL_WEBHOOK_SHARED_SECRET"].encode()


def verify(raw_body: bytes, signature: str) -> bool:
    digest = hmac.new(SHARED_SECRET, raw_body, hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    return hmac.compare_digest(expected, signature or "")

Commerce Layer webhook limitations and pain points

The circuit breaker disables failing webhooks

The Problem: Commerce Layer retries up to 10 times, notifies the owner after 5 failures, and after 30 consecutive failures disables the webhook (tracked via circuit_state / circuit_failure_count). A persistently failing endpoint stops receiving events and needs a manual reset.

Why It Happens: The circuit breaker protects Commerce Layer from repeatedly delivering to broken endpoints.

Workarounds:

  • Acknowledge with a 2xx fast so transient issues don't accumulate toward the 30-failure threshold.
  • Monitor circuit_state and reset the webhook after fixing an outage.

How Hookdeck Can Help: Hookdeck always accepts deliveries and absorbs downstream failures itself, keeping the circuit closed while it retries to your service, so an outage doesn't trip the breaker and disable your webhook.

The 5-second acknowledgement window

The Problem: You must return 2xx within 5 seconds. Any synchronous work (fulfillment calls, DB writes) risks the window and counts as a failure.

Why It Happens: Commerce Layer caps how long it waits for an ack.

Workarounds:

  • Verify, enqueue, and return 200 immediately; process off a queue.

How Hookdeck Can Help: Hookdeck acknowledges Commerce Layer within the window and durably queues events, decoupling the ack from your processing time.

Raw-body sensitivity

The Problem: The signature is over the raw body. Middleware that parses and re-serializes the JSON breaks verification.

Why It Happens: JSON reserialization doesn't preserve the exact signed bytes.

Workarounds:

  • Capture the raw body and verify before parsing.

How Hookdeck Can Help: Hookdeck verifies against the bytes as received, so your app never has to preserve the raw body itself.

Retries and duplicates

The Problem: Up to 10 retries mean the same event can arrive more than once, and double-processing an order is costly.

Why It Happens: At-least-once delivery favors eventual delivery.

Workarounds:

  • Dedupe on the resource/event ID and make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries don't double-process orders. See our guide to webhook idempotency.

Best practices

Verify with the webhook's shared_secret over the raw body

Compute base64 HMAC-SHA256 over the raw body with the shared_secret from the create response, and compare in constant time.

Acknowledge within 5 seconds, process asynchronously

Return 2xx immediately and defer work to a queue so you never approach the window or trip the circuit breaker. See why to process webhooks asynchronously.

Monitor the circuit breaker

Watch circuit_state / circuit_failure_count and reset a disabled webhook after resolving an outage.

Use include_resources and dedupe

Sideload related resources with include_resources to avoid follow-up API calls, and dedupe on the resource/event ID.

Make Commerce Layer webhooks production-ready

Hookdeck verifies X-CommerceLayer-Signature, keeps the circuit closed, deduplicates, and durably queues every event

Conclusion

Commerce Layer webhooks deliver {resource}.{trigger} events as JSON:API payloads, verified with an X-CommerceLayer-Signature HMAC over the raw body keyed with the webhook's shared_secret. The details that matter are the 5-second ack window and the circuit breaker that disables a webhook after 30 consecutive failures.

Hookdeck verifies the signature, keeps the circuit closed by absorbing downstream failures, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique commerce events without tripping the breaker.

Get started with Hookdeck for free and handle Commerce Layer webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.