Usage

This page describes what arrives at your URL and how to answer it. The full field schemas for every event are in the OpenAPI specification.

The request envelope

Every event arrives as a POST request with a JSON body in a single envelope:

JSON
{ "id": "0193...uuid", "event": "order.created", "occurred_at": "2026-05-11T10:00:00Z", "api_version": "1", "data": { } }
  • id: the delivery identifier, unchanged across retries of the same event.
  • event: the event name (see the list).
  • occurred_at: the moment of the event (ISO 8601, UTC).
  • api_version: the payload contract version.
  • data: the event-specific content. The schema is in the OpenAPI documentation.

Headers

Header Value
Content-Type application/json
User-Agent Mobius-Webhooks/1.0
X-Mobius-Event the event name
X-Mobius-Delivery the delivery identifier (the same for every attempt of one event)
X-Mobius-Timestamp unix seconds of the moment the request was generated
X-Mobius-Signature t=<timestamp>,v1=<hex hmac_sha256(timestamp + "." + body, secret)>

Verifying the signature

A subscriber must verify the signature and the timestamp. This protects against forged requests and against a replayed old request.

The algorithm:

  1. Take the raw request body, before parsing JSON.
  2. Parse t (the timestamp) and v1 (the signature) out of the X-Mobius-Signature header.
  3. Compute hmac_sha256(t + "." + raw_body, secret) with your subscription secret.
  4. Compare it with v1 in constant time.
  5. Reject the request if the signature does not match or t is older than 5 minutes.

A PHP example:

PHP
function verifyMobiusSignature(string $rawBody, string $header, string $secret): bool { // header: "t=1715420400,v1=abc123..." parse_str(str_replace(',', '&', $header), $parts); $t = $parts['t'] ?? ''; $v1 = $parts['v1'] ?? ''; if ($t === '' || $v1 === '') { return false; } // Replay protection: no older than 5 minutes. if (abs(time() - (int) $t) > 300) { return false; } $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); return hash_equals($expected, $v1); }

A Node.js example:

JS
const crypto = require('crypto'); function verifyMobiusSignature(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map(p => p.split('='))); const t = parts.t, v1 = parts.v1; if (!t || !v1) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = crypto .createHmac('sha256', secret) .update(`${t}.${rawBody}`) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); }

The secret comes from the admin panel, the Secret action in the event row (see Access and setup).

Idempotency

Async events can arrive more than once (your server answered, but the answer did not get through). Use X-Mobius-Delivery, the same as id in the envelope, as an idempotency key: if a delivery with that identifier has already been handled, return 2xx but do not perform the action a second time.

The response contract

Async events

order.created, order.paid, order.cancelled, bonus.*. Any 2xx is enough. The response body is not used.

Returning a non-2xx or failing to answer in time causes the delivery to be retried with a growing pause (60, 300, 900, 1800 seconds) up to the number of retries set on the subscription.

Sync events

order.calculate, order.bonus.calculate, catalog.bonus.calculate. The response affects the calculation, so it must be a 2xx with strictly one of two bodies:

JSON
{ "ok": true, "data": { } }
JSON
{ "ok": false, "error": { "code": "string", "message": "string" } }
  • With ok: true the data field carries the overridden values specific to the event (basket contents, totals, bonuses, see the OpenAPI documentation).
  • With ok: false, and with any rejection (a non-2xx, invalid JSON, a timeout), the calculation continues with the defaults and your response is not applied. The error is recorded in the log.

This is a soft contract: a subscriber's error never breaks checkout or catalogue output, it only means "use the defaults".

The delivery log

The Log tab in the webhooks section shows the delivery history: event, mode, status, response HTTP code, response time, attempt number and time. The Details action reveals the request and response bodies.

Every delivery attempt of an async event and every sync error land in the log. Successful sync calls (including the frequent catalog.bonus.calculate) are not logged, otherwise the log would overflow quickly.

Related pages

Updated 04.09.2026 18:04
Was this page helpful?