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:
{
"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:
- Take the raw request body, before parsing JSON.
- Parse
t(the timestamp) andv1(the signature) out of theX-Mobius-Signatureheader. - Compute
hmac_sha256(t + "." + raw_body, secret)with your subscription secret. - Compare it with
v1in constant time. - Reject the request if the signature does not match or
tis older than 5 minutes.
A PHP example:
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:
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:
{ "ok": true, "data": { } }
{ "ok": false, "error": { "code": "string", "message": "string" } }
- With
ok: truethedatafield 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
- Webhooks overview
- Access and setup
- OpenAPI documentation: the field schemas of every event.