# 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`:

```bash
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.

```javascript
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:

```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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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.

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Commerce Layer webhooks reliably in minutes.