# Guide to Emporix Webhooks: Features and Best Practices

Emporix webhooks tell your systems what is happening in an Emporix commerce tenant: an order is placed or cancelled, a customer signs up, a product or price changes, a cart is closed. If you run B2B commerce on Emporix, webhooks are how those changes reach your ERP, order management, search index and CRM without polling the Commerce Engine API.

This guide covers the three delivery strategies Emporix offers, how the default Svix strategy signs requests, the event types and what their bodies contain, the gaps in Emporix's documentation that affect a receiver, and the best practices for running one in production.

## What are Emporix webhooks?

Emporix's Webhook Service publishes Commerce Engine events and hands them to a delivery strategy you choose in the Management Dashboard. There are three strategies, and only one can be active at a time:

* **Shared account - Svix** is the default. Emporix delivers through its own Svix account, and you manage endpoints in a Svix dashboard that Emporix opens for you.
* **Licensed account - Svix** connects your own Svix subscription by API key. Delivery works the same way, and the monthly limit follows your Svix plan.
* **Other - HTTP** posts events directly to URLs you configure in the Management Dashboard, with an optional HMAC secret.

On both Svix strategies, Svix signs every request. Each delivery carries `svix-id`, `svix-timestamp` and `svix-signature` headers, and the signature is an HMAC-SHA256 of the message ID, the timestamp and the raw body, keyed with the endpoint's signing secret. Emporix's own documentation names Svix but never shows the headers or the secret, so the details in this guide come from Svix's documentation of its scheme.

The HTTP strategy signs differently. When a secret is set, it sends an `emporix-event-signature` header holding a base64 HMAC-SHA256 of the payload after Emporix re-serialises it with every key sorted alphabetically. That is not the raw body, so a receiver has to rebuild the canonical JSON before checking it. Hookdeck's Emporix source type supports the two Svix strategies only.

## Emporix webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Administration > Webhooks** in the Management Dashboard to choose a strategy and subscribe to events. Endpoints are added in the Svix dashboard on the Svix strategies |
| Strategies | Shared account - Svix (default), Licensed account - Svix, Other - HTTP. One active at a time |
| Signature headers (Svix) | `svix-id`, `svix-timestamp`, `svix-signature` |
| Scheme (Svix) | HMAC-SHA256, base64, sent as one or more space-separated `v1,<signature>` entries |
| Signed content (Svix) | `{svix-id}.{svix-timestamp}.{raw body}` |
| Secret (Svix) | One per endpoint, shown in the Svix dashboard, in Svix's `whsec_` format |
| HTTP strategy | Optional `emporix-event-signature`, a base64 HMAC-SHA256 over alphabetically sorted JSON, plus `emporix-event-publish-time` |
| Event type in the request | None documented. Route by endpoint |
| Volume limit | 5000 events per tenant per month on Svix-Shared. Beyond that, nothing is sent until the first of the next month |
| Retries | Svix's schedule on the Svix strategies. None documented for HTTP |
| IP allowlist | None published |

## Common events

Emporix documents 81 event types across 16 services. Each is a dotted string, and you switch each one on separately.

| Service | Events |
| --- | --- |
| Order | `order.created`, `order.updated`, `order.cancelled`, `order.status-changed`, `order.deleted` |
| Customer | `customer.created`, `customer.updated`, `customer.deleted`, `customer.sign-up`, `customer.logged.in`, `customer.opt-in`, `customer.password-reset`, `customer.refresh-token`, `customer.email-change`, `customer.email-change-request` |
| Cart | `cart.created`, `cart.updated`, `cart.merged`, `cart.closed`, `cart.item-added`, `cart.item-updated`, `cart.item-deleted` |
| Product and catalog | `product.created`, `product.updated`, `product.deleted`, `catalog.*`, `category.*`, `assignment.created`, `assignment.deleted` |
| Pricing | `price.*`, `price-model.*`, `price-list.*`, `price-list-price.*` (each `created`, `updated` and `deleted`) |
| Quotes and returns | `quote.created`, `quote.updated`, `quote.deleted`, `return.created`, `return.updated`, `return.deleted` |
| Availability | `availability.created`, `availability.updated`, `availability.deleted` |
| B2B accounts | `client-management.legalentity-*`, `client-management.location-*`, `client-management.user-assigned-to-legalentity`, `customer-segment.*` |
| Other | `index-item.updated`, `index-item.deleted`, `schema.custom-instance-*`, `site.site-*`, `vendor.vendor-*` |

Two details matter for routing. First, the index events have two names in Emporix's own docs: the Management Dashboard page calls one `index.item-updated`, while the event reference uses `index-item.updated`. Check the **Event catalog** in the Svix dashboard for the string your tenant uses. Second, `index-item.updated` fires once per site that a product's prices are attached to, so one product change can produce several events.

The body of each event is the entity itself (an order, a customer, a product), limited to the attributes you selected when you subscribed. Emporix documents no envelope and no `type` or `eventType` field, and Svix does not add the event type to the payload. A `customer.created` body might carry `id`, `customerNumber`, `contactEmail`, `firstName` and `lastName`, and nothing in it says it is a `customer.created` event rather than `customer.updated`. Emporix publishes field lists for each event rather than example bodies, so the first real delivery is your best reference for the exact shape.

> **Inspect Emporix webhooks as they arrive.** Point a Svix endpoint at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to inspect and replay real deliveries, with no account or setup required.

## Setting up Emporix webhooks

1. In the Management Dashboard, go to **Administration > Webhooks**, choose **Shared account - Svix** (or **Licensed account - Svix** with your Svix API key), and confirm with **Active**.
2. Choose **Open Svix Dashboard**. Emporix redirects you to Svix, where your tenant is the Svix application.
3. In Svix, go to **Endpoints > Add endpoint**, enter your HTTPS URL, choose the event types, and click **Create**.
4. Open the endpoint and copy its **Signing Secret**, which starts with `whsec_`. Store it server-side, for example as `EMPORIX_WEBHOOK_SECRET`. Each endpoint has its own secret.
5. Back in **Administration > Webhooks**, expand each service, switch on the events you want, and use the edit icon to choose which attributes appear in the payload.

Because the body carries no event type, give each event type its own endpoint and encode the type in the URL, for example `https://example.com/webhooks/emporix?eventType=order.created`. Your handler can then read the type from the query string or path.

To get the Svix dashboard link programmatically, `GET https://api.emporix.io/webhook/{tenant}/dashboard-access` returns a Svix magic login link.

If you receive Emporix webhooks through Hookdeck, use one of the Svix strategies, not the HTTP strategy, and paste the endpoint's signing secret from the Svix dashboard into the Hookdeck source. Hookdeck cannot verify the HTTP strategy's `emporix-event-signature`.

## Securing Emporix webhooks

Svix's scheme is the same one Standard Webhooks describes. Strip `whsec_` from the secret and base64-decode the rest to get the key. Build `{svix-id}.{svix-timestamp}.{raw body}`, compute an HMAC-SHA256 with that key, base64-encode it, and compare it in constant time with every `v1,` entry in `svix-signature`. Several entries appear while a secret is being rotated, and any match passes.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyEmporixSignature(rawBody, headers, secret) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader || !secret) return false;

  // The key is the base64-decoded part after "whsec_", not the string itself.
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest('base64');
  const computed = Buffer.from(expected, 'utf8');

  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'utf8');
    return received.length === computed.length && crypto.timingSafeEqual(received, computed);
  });
}

const app = express();

app.post('/webhooks/emporix', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyEmporixSignature(req.body, req.headers, process.env.EMPORIX_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  // Optional: reject if svix-timestamp is too far from now. Leave this off
  // behind Hookdeck, because retries and replays resend the original timestamp.

  const eventType = req.query.eventType; // set per endpoint in the Svix dashboard
  const entity = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on req.headers['svix-id'], persist, then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac

def verify_emporix_signature(raw_body: bytes, svix_id, svix_timestamp, svix_signature, secret: str) -> bool:
    if not (svix_id and svix_timestamp and svix_signature and secret):
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{svix_id}.{svix_timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
    for entry in svix_signature.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode(), expected):
            return True
    return False

```

You can also use the official `svix` library (`new Webhook(secret).verify(rawBody, headers)`) or the `standardwebhooks` library with the `svix-*` values passed as `webhook-*`. Both always reject a `svix-timestamp` more than 5 minutes from the current time, and the window cannot be turned off. Hookdeck retries and replays resend the original headers, so any replay of an older Emporix event through Hookdeck fails that check even though the signature is valid. The manual verifier above has no timestamp window, which is why the Emporix skill uses it.

Emporix never documents the header names its Svix deliveries use. Svix's defaults are `svix-*`, and Svix also supports `webhook-*` names. Log the headers of your first delivery to confirm which you receive. Hookdeck's Emporix source accepts either.

> **Make Emporix webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the Svix signature with your endpoint's `whsec_` secret, keeps every order, customer and product event in a durable queue, and lets you replay any of them without a timestamp window getting in the way.

## Emporix webhook limitations and pain points

### No event type in the request

**The Problem:** A single endpoint that receives several event types cannot tell a `cart.created` from a `cart.updated`, because both bodies are a cart.

**Why It Happens:** Emporix documents each event's payload as the entity alone, with no envelope. Svix does not inject the event type into the payload, and no event-type header is documented.

**Workarounds:**

* Create one Svix endpoint per event type and put the type in the URL, as a path segment or query parameter.
* Log the full headers and body of the first delivery of each type, in case a type carrier turns up that the docs do not mention.

**How Hookdeck Can Help:** Point every Svix endpoint at one Hookdeck source with a different query string, then use [filters](/docs/filters) on the query or path to route each event type to its own destination.

### The HTTP strategy signs re-serialised JSON

**The Problem:** A tenant on the HTTP strategy finds that a standard HMAC of the raw body never matches `emporix-event-signature`.

**Why It Happens:** Emporix signs a canonical JSON serialisation, with keys sorted alphabetically at every level and integers written as integers, not the bytes it sent. It also signs no timestamp, so there is no replay protection.

**Workarounds:**

* Use a Svix strategy where you can, which signs the raw body and a timestamp.
* If you must stay on HTTP, rebuild the canonical JSON with a stable stringifier before computing the HMAC, as Emporix's own sample does.

**How Hookdeck Can Help:** Hookdeck's Emporix source verifies the Svix strategies. For an HTTP-strategy tenant, Hookdeck can still receive, queue and replay the events, and the canonical check stays in your code.

### A monthly cap on the default strategy

**The Problem:** On Svix-Shared, events stop arriving partway through a month and nothing errors.

**Why It Happens:** Emporix limits Svix-Shared to 5000 events per tenant per month. Once you exceed it, requests are not sent until the limit resets on the first of the month. Svix dashboard test events count too.

**Workarounds:**

* Watch **Webhook usage** under **Administration > Webhooks**, and alert well before 5000.
* Subscribe only to the events and attributes you need. `index-item.updated`, which fires once per site, can use up the allowance quickly.
* Move to the licensed Svix strategy, or contact Emporix sales, if your volume is higher.

**How Hookdeck Can Help:** Hookdeck's request log gives you a running count of what Emporix sent to each source, so you can see the volume climbing before deliveries stop.

### Library timestamp checks break replays

**The Problem:** Events verify on first delivery, but retries and replays that arrive later fail with a timestamp error.

**Why It Happens:** The `svix` and `standardwebhooks` libraries reject any `svix-timestamp` more than 5 minutes old, and a retry or replay keeps the original timestamp.

**Workarounds:**

* Verify manually with no timestamp window, as shown above, and rely on deduplication by `svix-id` for replay protection.
* If you want a window, make it wider than your longest retry or replay delay.

**How Hookdeck Can Help:** Hookdeck verifies the signature when the request arrives and records the result, so you can [replay](/docs/events) a stored event to your destination later without it failing on age.

## Best practices

### Verify against the raw body

The signature covers the exact bytes Svix sent. In Express, mount `express.raw({ type: '*/*' })` on the route. In Next.js, use `await request.text()`. In FastAPI, read `await request.body()`. Parse only after verification passes.

### Decode the secret

The HMAC key is the base64-decoded bytes after `whsec_`. Using the whole string as the key produces a valid-looking signature that never matches. Each Svix endpoint has its own secret, so a secret copied from another endpoint fails too.

### Deduplicate on `svix-id`

Svix keeps `svix-id` the same across retries of one message, which makes it the natural idempotency key. Store it before processing. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Return a `2xx` as soon as the request is verified and stored, then do the ERP or search update in the background. Svix's documentation treats a `2xx` within a reasonable time (30 seconds with Svix) as success, and retries anything else on its schedule of 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. Emporix does not state whether its Svix account changes those defaults. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Trim payloads at the source

You choose which attributes each event carries when you subscribe. Subscribe only to the fields you use, and fetch the full entity from the Emporix API when you need more. Smaller payloads are easier to store and replay.

## Conclusion

Emporix delivers webhooks through Svix by default, so verification follows Svix's scheme: an HMAC-SHA256 over `{svix-id}.{svix-timestamp}.{raw body}`, keyed with the decoded `whsec_` secret from the Svix dashboard. Because the body carries no event type, register an endpoint per event type, deduplicate on `svix-id`, keep an eye on the 5000-event monthly cap, and avoid libraries whose fixed timestamp window rejects late retries.

[Hookdeck Event Gateway](https://hookdeck.com) checks every Svix signature from Emporix, routes each event type by URL, and holds events until your service has processed them, so an outage on your side does not cost you orders.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Emporix webhooks reliably in minutes.