Webhook catalog

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

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.

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 your endpoint URLs are 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 changes2xx
CLAIM_REPLACEMENTA claim is marked for replacement2xx 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

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.

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

CLAIM_REPLACEMENT

Fires 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 set

For legacy reasons, Mulberry only sends CLAIM_REPLACEMENT when both warranty_claim_url and order_success_url are configured. If order_success_url is unset, no replacement webhook is delivered. See Configuration.

👍

Acknowledge with a confirmation body

A bare 2xx is not enough for this event. Mulberry only counts the delivery as successful when your response is a 2xx and its JSON body contains {"replacement_ok": true}. Any other response — including a 2xx without 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:

HeaderValue
Content-Typeapplication/json
X-Requested-Withfetch
X-Webhook-TopicThe event name — ORDER_SUCCESS, CLAIM_UPDATE, or CLAIM_REPLACEMENT
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") ...

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

SettingPurpose
is_enabledMaster on/off switch. While false, Mulberry sends nothing
order_success_urlEndpoint for ORDER_SUCCESS. Also required for CLAIM_REPLACEMENT to be sent
claim_update_urlEndpoint for CLAIM_UPDATE
warranty_claim_urlEndpoint for CLAIM_REPLACEMENT
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

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 seconds

So 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 handlers

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

Related