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
{
"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
{
"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:
- Extract the timestamp (
t) and signature (v1) from the header. - Compute the expected signature:
HMAC-SHA256(secret, "TIMESTAMP.BODY")whereBODYis the raw request body. - Compare your computed signature with the
v1value.
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
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
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.