Receive webhooks
Register an endpoint, verify signatures, and process events once.
Webhooks notify your backend when checkout or refund state changes. Register a public HTTPS endpoint, verify every signed request, and process each event identifier once.
Register a webhook endpoint
Ask a merchant owner or administrator to register the callback URL in the MB Wallet Merchant Console. Register the endpoint in the same mode as the API key and checkout data.
Your endpoint must meet these requirements:
- Use a public HTTPS URL on port
443 - Accept
POSTrequests withContent-Type: application/json - Preserve the raw request body for signature verification
- Return a
2xxresponse within 10s after durable acceptance - Avoid redirects because MB Wallet does not follow
3xxresponses
The Merchant Console returns a signing secret beginning with mbsec_. Store it in your server-side secret manager. Do not expose it in client code or logs.
Register every destination
Setting notify_url on a checkout request does not replace webhook endpoint registration in the Merchant Console.
Understand the event payload
Every delivery contains an event envelope. The id identifies one event delivery, while data.object.id identifies the affected resource.
{
"id": "MB-EVENT-7K2M9Q4R",
"object": "event",
"type": "checkout.session.captured",
"created": 1784772000,
"data": {
"object": {
"id": "MB-PAYMENT-4F9K2A7Q"
}
}
}The current event types are:
| Event type | Meaning | Resource identifier |
|---|---|---|
checkout.session.captured | A checkout payment was captured | MB-PAYMENT-… |
refund.posted | A refund was posted | MB-REFUND-… |
Verify the signature
MB Wallet signs the exact request body with a hash-based message authentication code (HMAC) using SHA-256. Verify the raw bytes before parsing JSON.
The Mb-Webhook-Signature header has this format:
Mb-Webhook-Signature: t=1784772000,v1=signature_hex_hereCompute the expected signature over these bytes:
timestamp.raw_request_bodyThis TypeScript function verifies the timestamp and every v1 signature in the header. Multiple signatures may appear during secret rotation.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyMbWebhook(
rawBody: Buffer,
header: string,
secret: string,
): boolean {
const parts = header.split(",");
const timestamp = parts.find((part) => part.startsWith("t="))?.slice(2);
const signatures = parts
.filter((part) => part.startsWith("v1="))
.map((part) => part.slice(3));
if (!timestamp || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
return signatures.some((value) => /^[0-9a-f]{64}$/.test(value)
&& timingSafeEqual(expected, Buffer.from(value, "hex")));
}Reject signatures more than 300s from your server clock. Keep your server clock synchronized and compare signatures with a constant-time function.
Process each event once
MB Wallet may deliver the same event more than once. Make your webhook handler idempotent with this order:
- Read the raw request body
- Verify
Mb-Webhook-Signature - Parse the JSON payload
- Insert the event
idinto a durable table with a unique constraint - Apply the business change in the same transaction
- Return
2xxafter the transaction commits
If the event id already exists, return 2xx without applying the change again. Do not deduplicate by the resource identifier because one resource can produce multiple events.
A manual redrive creates a new event id and includes redrive_of with the earlier event identifier. Process the redrive as a new delivery, while retaining the reference for audit.
Handle retries and failures
Return 2xx only after you have stored enough information to resume processing. MB Wallet treats any 2xx response as successful delivery.
Retryable failures include network errors, timeouts, 408, 425, 429, and server errors. MB Wallet retries them with backoff and honors Retry-After for up to 24 hours.
Redirects and most other client errors stop automatic delivery. Update the registered endpoint or request a redrive after correcting the problem.
Separate test and live webhooks
Use different endpoint registrations and signing secrets for test and live mode. Test events never authenticate with a live webhook secret.
Start with a staging URL such as https://staging.merchant.example/webhooks/mbwallet. Move to your production callback only after signature verification, duplicate delivery, and retry tests pass.