Webhooks

Receive real-time notifications when events happen in your account.

How Webhooks Work

When an event occurs (e.g. a payment completes), Mozart Pay sends an HTTP POST request to each webhook endpoint you've configured. The request body contains a JSON payload describing the event.

You can configure webhook endpoints from your app's dashboard under Webhooks.

Event Types

Event Description
payment.updated A payment's status has changed (e.g. pending to completed, or pending to failed).
refund.completed A refund was processed successfully.
refund.failed A refund attempt failed at the gateway.

Payload Format

Payment Event

JSON
{
  "event": "payment.updated",
  "payment": {
    "id": 42,
    "gateway": "stripe",
    "status": "completed",
    "amount_cents": 2500,
    "currency": "GBP",
    "external_id": "pi_3abc123",
    "metadata": { "order_id": "ORD-123" },
    "created_at": "2026-01-15T10:30:00.000Z"
  }
}

Refund Event

JSON
{
  "event": "refund.completed",
  "refund": {
    "id": 7,
    "payment_id": 42,
    "amount_cents": 2500,
    "status": "completed",
    "reason": "Customer requested",
    "external_id": "re_abc123",
    "created_at": "2026-01-16T14:00:00.000Z"
  },
  "payment": {
    "id": 42,
    "gateway": "stripe",
    "status": "completed",
    "amount_cents": 2500,
    "currency": "GBP"
  }
}

Headers

Each webhook request includes the following headers:

Header Description
Content-Type application/json
Mozart-Pay-Signature HMAC signature for verifying authenticity (see below).
Mozart-Pay-Timestamp Unix timestamp when the webhook was sent.

Verifying Webhook Signatures

Every webhook is signed using your app's webhook signing secret with HMAC-SHA256. You should verify this signature to ensure the webhook was sent by Mozart Pay and not a third party.

The Mozart-Pay-Signature header has the format: t=TIMESTAMP,v1=SIGNATURE

To verify:

  1. Extract the timestamp (t) and signature (v1) from the header.
  2. Compute the expected signature: HMAC-SHA256(secret, "TIMESTAMP.BODY") where BODY is the raw request body.
  3. Compare your computed signature with the v1 value.

Ruby

Ruby
def verify_webhook(payload, signature_header, secret)
  parts = signature_header.split(",").to_h { |p| p.split("=", 2) }
  timestamp = parts["t"]
  signature = parts["v1"]

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{payload}")
  ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end

Node.js

JavaScript
const crypto = require("crypto");

function verifyWebhook(payload, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${payload}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}

Python

Python
import hmac
import hashlib

def verify_webhook(payload, signature_header, secret):
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    expected = hmac.new(
        secret.encode(), f"{parts['t']}.{payload}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Retry Policy

If your endpoint returns a non-2xx response or fails to respond, Mozart Pay retries the delivery up to 5 times with exponential backoff. After 5 failed attempts, the delivery is marked as failed and you'll receive an email alert.

You can view delivery status and manually retry failed deliveries from your app's webhook dashboard.

Tip: Your webhook signing secret is available on the Webhooks page in your dashboard. You can regenerate it at any time, but this will invalidate the previous secret.