Webhook catalog

The outbound webhooks Mulberry sends, their payloads, and how to verify their signatures.

Mulberry pushes outbound webhooks to your endpoints so you can react to sales and claims in real time: ORDER_SUCCESS when a sale is confirmed, and CLAIM_UPDATE, set up on request, when a claim's status changes. 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 it

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

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 POST requests for the events you care about.
  • Webhook delivery is enabled for your account and the URL for each event you use is configured (see Configuration). Mulberry sends nothing while delivery is disabled.

The events

Event (X-Webhook-Topic)Fires whenYou must respond with
ORDER_SUCCESSA warranty sale is confirmed at checkout2xx
CLAIM_UPDATEA claim's status changes (set up on request)2xx

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

Fires 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 on external_order_id

external_order_id is 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

Fires on any claim status change. Use it to keep your records in sync with the claim lifecycle. Delivered to your claim_update_url, which Mulberry sets up on request — claims need nothing built on your side, so you only need this event if you mirror claim status in your own systems.

❗

A system-closed claim still sends a CLAIM_UPDATE

When a cancellation closes an open claim, Mulberry does send a CLAIM_UPDATE — with status CLOSED and reason CLOSED_CANCELLED_BY_CUSTOMER. Don't assume system closures are silent; handle a CLOSED status the same way you handle one a human triggered. Which cancellations close open claims is covered in Cancel a warranty.

{
  "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:

  • status is the claim status name, such as SUBMITTED, UNDER_REVIEW, APPROVED, DENIED, RESOLVED, or CLOSED. Other values exist; treat unknown statuses as forward-compatible additions, not errors.
  • warranty.status is the warranty's lifecycle status — one of Issued, Fulfilled, Cancelled, or Expired (note the title-case).
  • reason and payout_amount are null until they apply to the claim.
  • order.external_order_id is the order ID you supplied; order.id is 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.

Request headers

Every webhook request carries these headers:

HeaderValue
Content-Typeapplication/json
X-Requested-Withfetch
X-Webhook-TopicThe event name — ORDER_SUCCESS or CLAIM_UPDATE
X-Signature-SHA256Base64-encoded HMAC-SHA256 of the raw request body (see below)
X-Idempotency-KeyThe event's event_id, when the payload includes one. Use it to deduplicate retried deliveries
AuthorizationBearer <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.

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

  2. Compute HMAC-SHA256 over that raw body using your mulberry_access_token as the secret key, then base64-encode the digest.

    You should now have a base64 string.

  3. Compare it against the X-Signature-SHA256 header 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 object

Mulberry 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") ...

    return res.sendStatus(200);
  });

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.

SettingPurpose
is_enabledMaster on/off switch. While false, Mulberry sends nothing
order_success_urlEndpoint for ORDER_SUCCESS
claim_update_urlEndpoint for CLAIM_UPDATE. Set on request
retailer_auth_tokenOptional. When set, Mulberry adds Authorization: Bearer <token> to every request
max_retriesMaximum retry attempts per event. When unset (or 0), failed deliveries are not retried. See Retries

To change any of these, contact your Mulberry representative. CLAIM_UPDATE is off unless you ask for it; when you do, give the URL for each environment, since staging and production are configured separately.

Retries

When a delivery fails — a timeout (Mulberry waits up to 3 seconds for your response), a connection error, or any non-2xx response — Mulberry queues it for retry if max_retries is set. When max_retries is unset, the delivery is not retried.

Queued deliveries are retried by a job that runs every 15 minutes. Each retry goes out at the first run that's at least 1, 2, 4, 8, 16, … minutes after the previous attempt, doubling each time. With max_retries set to 5, that works out to:

RetrySent
1stAt the next run, up to about 15 minutes after the failed delivery
2nd, 3rd, 4th15 minutes after the previous attempt
5th30 minutes after the previous attempt

So the last retry goes out about 75 to 90 minutes after the original delivery. If it fails too, the event is dropped.

📘

Build idempotent handlers

Because deliveries can be retried, your endpoint may receive the same event more than once. Deduplicate CLAIM_UPDATE on the X-Idempotency-Key header (the event's event_id) and ORDER_SUCCESS, which carries no event_id, on its order_id, so reprocessing a delivery is harmless.

Related