Guide to Commerce Layer Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | POST /api/webhooks (topic, callback_url, optional include_resources) |
| Signature header | X-CommerceLayer-Signature |
| Signature scheme | base64 HMAC-SHA256 over the raw body, keyed with the webhook's shared_secret |
| Topic format | {resource}.{trigger} (e.g. orders.place) |
| Payload | JSON:API resource (same shape as the REST API) |
| Acknowledgement | Return 2xx within 5 seconds |
| Circuit breaker | Up to 10 retries; owner notified after 5 failures; disabled after 30 consecutive failures |
| State fields | circuit_state, circuit_failure_count |
| SDK | @commercelayer/sdk (official JS) |
Common topics
Topics combine a resource and a trigger as {resource}.{trigger}:
| Topic | Fires when |
|---|---|
orders.place | An order is placed |
orders.approve | An order is approved |
returns.request | A 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_stateand 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.