Skip to main content
Maash sends webhook notifications to your server at every stage of the checkout lifecycle. Webhooks use HMAC-SHA256 signatures for security and include automatic retries on failure.

Setting up webhooks

1

Configure your webhook URL

Contact the Maash team to register your webhook_url. All checkout events will be delivered to this URL once configured.
2

Ensure your endpoint meets requirements

Your endpoint must:
  • Accept POST requests with a JSON body
  • Return a 2xx status code within 30 seconds
  • Be publicly accessible over HTTPS
3

Verify webhook signatures

Verify the X-Maash-Signature header on every delivery to confirm authenticity. See Verifying signatures below.

Webhook payload

Every webhook delivery uses a consistent envelope format with an event, action, and body:
{
  "id": "wh_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "event": "checkout",
  "action": "create",
  "timestamp": "2024-01-31T15:30:00Z",
  "version": "1.0.0",
  "body": {
    "transaction_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "merchant_id": "mer_abc123",
    "ext_transaction_id": "order_789",
    "amount": "100.00",
    "currency": "USD",
    "status": "completed"
  }
}

Top-level fields

FieldTypeDescription
idstringUnique webhook identifier (ULID with wh_ prefix).
eventstringAlways "checkout".
actionstring"create" for new sessions, "update" for all subsequent changes.
timestampstringISO 8601 timestamp of when the webhook was generated.
versionstringPayload schema version. Currently "1.0.0".

Body fields

FieldTypeDescription
transaction_idstringMaash transaction ID for this checkout session.
merchant_idstringYour Maash merchant identifier.
ext_transaction_idstringYour external transaction ID (passed when creating the session).
amountstringPayment amount in USD.
currencystringCurrency code. Always "USD".
statusstringCurrent checkout status (see table below).

Statuses and actions

Webhooks are sent at each stage of the checkout lifecycle:
StatusActionTrigger
initcreateCheckout session created.
awaiting_paymentupdateCustomer selected a token and chain for payment.
pendingupdatePayment detected on-chain, bridge pending.
processingupdateBridge accepted, funds being transferred.
completedupdatePayment confirmed and settled to your wallet.
failedupdatePayment failed, refunded, cancelled, or rejected.

Webhook headers

Each webhook request includes these headers:
HeaderDescription
Content-Typeapplication/json
X-Maash-SignatureHMAC-SHA256 signature of the payload.
X-Maash-TimestampUnix timestamp of when the webhook was sent.
X-Maash-Idempotency-KeyUnique key in the format {transaction_id}_{status}_v1.

Verifying signatures

Verify the X-Maash-Signature header to confirm that a webhook came from Maash. The signature is computed as:
HMAC-SHA256(timestamp + "." + raw_body, webhook_secret)
import crypto from "crypto";

function verifyWebhookSignature(req, webhookSecret) {
const signature = req.headers["x-maash-signature"];
const timestamp = req.headers["x-maash-timestamp"];
const body = JSON.stringify(req.body);

const expectedSignature = crypto
.createHmac("sha256", webhookSecret)
.update(`${timestamp}.${body}`)
.digest("hex");

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}

Always use constant-time comparison (like timingSafeEqual or hmac.compare_digest) to prevent timing attacks.

Timestamp validation

Reject webhooks with timestamps older than 5 minutes to prevent replay attacks:
const timestamp = parseInt(req.headers["x-maash-timestamp"]);
const now = Math.floor(Date.now() / 1000);

if (Math.abs(now - timestamp) > 300) {
  // Reject — timestamp is too old or too far in the future
  return res.status(400).send("Invalid timestamp");
}

Retry policy

If your endpoint returns a non-2xx status code or does not respond within 30 seconds, Maash retries the delivery with exponential backoff:
AttemptDelay
1Immediate
21 minute
35 minutes
415 minutes
51 hour
6 (final)2 hours
After 6 failed attempts, the webhook is moved to a dead letter queue. You can view failed deliveries and manually retry them from the dashboard.

Idempotency

Each webhook delivery includes an X-Maash-Idempotency-Key header. Use this key to deduplicate events in case of retries. The key format is {transaction_id}_{status}_v1. For example: cs_01ARZ3NDEKTSV4RRFFQ69G5FAV_completed_v1.
app.post("/webhooks/maash", async (req, res) => {
  const idempotencyKey = req.headers["x-maash-idempotency-key"];

  // Check if you've already processed this event
  const alreadyProcessed = await db.webhookEvents.findOne({ idempotencyKey });
  if (alreadyProcessed) {
    return res.status(200).send("Already processed");
  }

  // Process the event
  const { action, body } = req.body;
  if (action === "create") {
    await handleNewCheckout(body);
  } else {
    await handleCheckoutUpdate(body);
  }

  // Record the event
  await db.webhookEvents.create({ idempotencyKey, processedAt: new Date() });

  res.status(200).send("OK");
});

Best practices

Return a 200 response as soon as you receive the webhook, then process the event asynchronously. This avoids timeouts and unnecessary retries.
Always validate the X-Maash-Signature before processing. Reject any delivery that fails verification.
Use the X-Maash-Idempotency-Key to prevent double-processing. Store processed keys and skip events you have already handled.
Store the raw payload for debugging and auditing. This helps diagnose issues if a webhook handler behaves unexpectedly.
Webhook URLs must use HTTPS in production. HTTP endpoints will be rejected.

Next steps

Checkout flow

Understand the full payment lifecycle and session statuses.

Error handling

Handle API errors and implement retry strategies.

Testing

Test webhook delivery and signature verification locally.

Authentication

Learn about API keys and session tokens.