The three outbound webhooks Mulberry sends, their payloads, and how to verify their signatures.
Mulberry pushes three outbound webhooks to your endpoints so you can react to sales and claims in real time. Use this page to learn which events fire, the payload each one carries, and how to verify that a request genuinely came from Mulberry.
Verify every request before trusting itEvery webhook is signed with an HMAC over the raw request body. Recompute the signature yourself and compare it before acting on the payload — see Verify the signature. Never act on an unverified request.
A system-closed claim still sends aCLAIM_UPDATEWhen a cancellation closes an open claim, Mulberry does send a
CLAIM_UPDATE— withstatusCLOSEDandreasonCLOSED_CANCELLED_BY_CUSTOMER. Don't assume system closures are silent; handle aCLOSEDstatus the same way you handle one a human triggered. Which cancellations close open claims is covered in Cancel a warranty.
Prerequisites
Before you can receive webhooks, confirm:
- You have your retailer credentials, including the
mulberry_access_token— this token is the HMAC signing secret. See Get your credentials. - You have HTTPS endpoints ready to receive
POSTrequests for the events you care about. - Webhook delivery is enabled for your account and your endpoint URLs are configured (see Configuration). Mulberry sends nothing while delivery is disabled.
The events
Event (X-Webhook-Topic) | Fires when | You must respond with |
|---|---|---|
ORDER_SUCCESS | A warranty sale is confirmed at checkout | 2xx |
CLAIM_UPDATE | A claim's status changes | 2xx |
CLAIM_REPLACEMENT | A claim is marked for replacement | 2xx and {"replacement_ok": true} |
Each event is delivered as an HTTP POST with a JSON body and the headers described in Request headers. Mulberry treats any non-2xx response as a failed delivery and may retry it (see Retries).
ORDER_SUCCESS
ORDER_SUCCESSFires when a warranty sale is confirmed at checkout. Delivered to your order_success_url; acknowledge with a 2xx. The body is an order summary tied to the sale you registered. The exact field set is established by your registration, so treat the shape below as illustrative and confirm it against a live delivery — correlate events to your records using the external_order_id you supplied.
{
"order_id": "1029384",
"external_order_id": "your-order-1234",
"created_date": "2026-01-15T18:00:00",
"order_status_value": "Active",
"order_source": "...",
"customer": {
"first_name": "...",
"last_name": "...",
"email": "..."
},
"line_items": [
{
"created_date": "2026-01-15T18:00:00",
"warranty": { "...": "..." },
"products": [ { "...": "..." } ]
}
]
}Top-level fields: order_id (Mulberry's internal order ID, as a string), external_order_id (the order ID you supplied), created_date, order_status_value, order_source, customer, and line_items (the warranties registered on the order, each with its warranty and products).
Correlate onexternal_order_id
external_order_idis the ID you supplied when you registered the sale, so use it to match the event back to your own order. See Register the sale.
CLAIM_UPDATE
CLAIM_UPDATEFires on any claim status change. Use it to keep your records in sync with the claim lifecycle. Delivered to your claim_update_url.
{
"event_id": "1718553600000-9f3c8e2a4b1d4f6a8c0e2d4f6a8c0e2d",
"event_name": "CLAIM_UPDATE",
"data": {
"claim": {
"id": "8c0e2d4f-6a8c-4e2d-9f3c-8e2a4b1d4f6a",
"customer": {
"id": "2d4f6a8c-0e2d-4f6a-8c0e-2d4f6a8c0e2d"
},
"status": "APPROVED",
"reason": "FINANCE_APPROVED_REPLACEMENT",
"payout_amount": "149.99",
"issued_replacement": true,
"order": {
"id": 1029384,
"external_order_id": "your-order-1234"
},
"warranty": {
"id": 5566778,
"status": "Issued",
"duration_months": 24,
"start_date": "2026-01-15",
"end_date": "2028-01-15",
"policy_terms_url": "https://...",
"coverage_details": "..."
},
"product": {
"title": "Sectional Sofa",
"variant_title": "Charcoal",
"price": "1299.00",
"currency": "USD",
"external_product_id": "sku-9988",
"external_variant_id": "var-7766"
}
}
}
}Field notes:
statusis the claim status name, such asSUBMITTED,UNDER_REVIEW,APPROVED,DENIED,RESOLVED, orCLOSED. Other values exist; treat unknown statuses as forward-compatible additions, not errors.warranty.statusis the warranty's lifecycle status — one ofIssued,Fulfilled,Cancelled, orExpired(note the title-case).reasonandpayout_amountarenulluntil they apply to the claim.order.external_order_idis the order ID you supplied;order.idis Mulberry's internal order ID.- Match incoming events to claims by
data.claim.id(the claim UUID). To react to specific statuses, see React to claim updates.
Acknowledge with a 2xx.
CLAIM_REPLACEMENT
CLAIM_REPLACEMENTFires when a claim is marked for replacement, signalling that you should fulfill a replacement unit. Delivered to your warranty_claim_url.
Both URLs must be setFor legacy reasons, Mulberry only sends
CLAIM_REPLACEMENTwhen bothwarranty_claim_urlandorder_success_urlare configured. Iforder_success_urlis unset, no replacement webhook is delivered. See Configuration.
Acknowledge with a confirmation bodyA bare
2xxis not enough for this event. Mulberry only counts the delivery as successful when your response is a2xxand its JSON body contains{"replacement_ok": true}. Any other response — including a2xxwithout that field — is treated as a failed delivery and may be retried.
The payload carries the order, customer shipping details, the warranty, and the product to replace, so you can route the replacement to fulfillment.
Respond with:
{ "replacement_ok": true }Request headers
Every webhook request carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Requested-With | fetch |
X-Webhook-Topic | The event name — ORDER_SUCCESS, CLAIM_UPDATE, or CLAIM_REPLACEMENT |
X-Signature-SHA256 | Base64-encoded HMAC-SHA256 of the raw request body (see below) |
X-Idempotency-Key | The event's event_id, when the payload includes one. Use it to deduplicate retried deliveries |
Authorization | Bearer <token> — present only if you configured a retailer_auth_token |
Verify the signature
Mulberry signs each request so you can confirm it is authentic and unmodified.
-
Read the raw, unparsed request body as bytes. Do not re-serialize the JSON — Mulberry signs the body exactly as sent (compact JSON, no spaces between tokens), and any reformatting will change the signature.
You should now have the exact byte string that was transmitted.
-
Compute
HMAC-SHA256over that raw body using yourmulberry_access_tokenas the secret key, then base64-encode the digest.You should now have a base64 string.
-
Compare it against the
X-Signature-SHA256header using a constant-time comparison.The two values should be identical. If they match, the request is authentic — process it. If they don't, reject the request (respond
401) and do not act on the payload.
Sign the raw body, not a re-parsed objectMulberry serializes the body with no whitespace (e.g.
{"a":1,"b":2}). If your framework parses the body into an object and you re-serialize it, the bytes — and therefore the signature — will differ. Capture the raw body before any JSON middleware touches it.
Example: Node.js (Express)
const crypto = require("crypto");
// Capture the RAW body — e.g. express.raw({ type: "application/json" })
function verifyMulberrySignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // rawBody is a Buffer / exact bytes received
.digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhooks/mulberry",
express.raw({ type: "application/json" }),
(req, res) => {
const ok = verifyMulberrySignature(
req.body, // Buffer of the raw body
req.get("X-Signature-SHA256"),
process.env.MULBERRY_ACCESS_TOKEN, // your mulberry_access_token
);
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event by req.get("X-Webhook-Topic") ...
// CLAIM_REPLACEMENT requires this exact body:
return res.status(200).json({ replacement_ok: true });
});Example: Python
import base64
import hashlib
import hmac
def verify_mulberry_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
digest = hmac.new(
secret.encode("utf-8"),
raw_body, # exact bytes received, not a re-serialized dict
hashlib.sha256,
).digest()
expected = base64.b64encode(digest).decode("utf-8")
return hmac.compare_digest(expected, signature_header or "")Configuration
Webhook delivery is configured per retailer. Mulberry sends an event only when delivery is enabled and the URL for that event's type is set.
| Setting | Purpose |
|---|---|
is_enabled | Master on/off switch. While false, Mulberry sends nothing |
order_success_url | Endpoint for ORDER_SUCCESS. Also required for CLAIM_REPLACEMENT to be sent |
claim_update_url | Endpoint for CLAIM_UPDATE |
warranty_claim_url | Endpoint for CLAIM_REPLACEMENT |
retailer_auth_token | Optional. When set, Mulberry adds Authorization: Bearer <token> to every request |
max_retries | Maximum retry attempts per event. When unset (or 0), failed deliveries are not retried |
To change any of these, contact your Mulberry representative.
Retries
When a delivery fails — a timeout, a connection error, or any non-2xx response (or, for CLAIM_REPLACEMENT, a response missing {"replacement_ok": true}) — Mulberry retries it if max_retries is set, using exponential backoff:
delay = 2 ^ retry_count * 60 secondsSo retries are spaced roughly 60s, 120s, 240s, and so on, until the delivery succeeds or max_retries is reached. After the final failed attempt the event is dropped.
Build idempotent handlersBecause deliveries can be retried, your endpoint may receive the same event more than once. Deduplicate on the
X-Idempotency-Keyheader (the event'sevent_id) so reprocessing a delivery is harmless.
Related
- Get your credentials — where your signing secret comes from
- Register the sale — registering a sale that triggers
ORDER_SUCCESS - React to claim updates — handling
CLAIM_UPDATE
