Webhooks
MutoPay sends an HMAC-SHA256 signed webhook when a payment completes, fails, expires, or needs attention. This page documents every event, the payload shape, and the delivery/retry guarantees.
MutoPay sends a webhook to your configured URL whenever a payment changes to a terminal state or requires your attention. Payloads are signed with HMAC-SHA256 — always verify the signature before trusting the data. See Verify a webhook signature.
Configure
Set the webhook URL and retrieve the webhook secret per-channel at Settings → Channels. Each channel has its own URL and secret.
Events
| Event | When it fires |
|---|---|
payment.completed | Payment succeeded, funds settled in your wallet |
payment.failed | Payment attempt failed (on-chain revert, provider rejection, etc.) |
payment.expired | Payment was not completed before expires_at |
payment.underpaid | Customer sent less than the required amount |
payment.kyc_required | The swap provider requested customer KYC — payment is paused, not terminal |
payment.needs_manual_check | Unknown provider status — MutoPay support will investigate |
kyc_required and needs_manual_check are non-terminal. When they resolve (e.g. KYC cleared → processing → completed), no additional webhook fires for the recovery — the eventual payment.completed or payment.failed is your signal.
Payload
{
"event": "payment.completed",
"payment_id": "pay_abc123",
"status": "completed",
"amount_usd": 54.23,
"amount_original": 50,
"fx_rate": 1.0846,
"currency": "EUR",
"src_token": "USDC",
"src_chain_id": "56",
"src_amount": null,
"dest_token": "USDC",
"dest_chain_id": "137",
"dest_amount": "54230000",
"dest_decimals": 6,
"external_id": "order_1042",
"tx_hash": "0xabc...",
"failure_reason": null,
"completed_at": "2026-04-21T12:00:00Z",
"timestamp": "2026-04-21T12:00:01Z"
}
Field notes
dest_amountis raw token base units. Divide by10^dest_decimalsfor human-readable:54230000 / 10^6 = 54.23 USDC.external_idis the value you sent when creating the payment — use it for reconciliation.tx_hashis the on-chain destination transaction (where the stablecoin arrived in your wallet).failure_reasonis populated forpayment.failedand sometimespayment.expired.timestampis when MutoPay fired the webhook.completed_atis when settlement actually happened.
Delivery guarantees
-
Delivery is at-least-once. You may receive the same webhook more than once — idempotency is your responsibility (use
payment_id+eventas the dedupe key). -
Your endpoint must respond with a 2xx status within 30 seconds to be considered delivered.
-
Non-2xx responses, timeouts, and network errors are retried up to 5 times with exponential backoff:
Attempt Delay after previous failure 2 1 minute 3 5 minutes 4 30 minutes 5 2 hours (final) 12 hours After the final attempt fails, delivery is abandoned. Use the merchant dashboard or
GET /api/merchant/payments/:idto reconcile.
Request headers
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-MutoPay-Signature: sha256=<hex-digest>
User-Agent: MutoPay-Webhook/1.0
Security
- Always verify the signature before processing the payload. See Verify a webhook signature.
- Never log the webhook secret. Rotate it via the channel settings if you suspect compromise.
- Your endpoint should be HTTPS.
Test your endpoint
From Settings → Channels click Send test webhook. MutoPay will POST a signed sample payload to your configured URL — same shape, same headers, same signing algorithm as production.
See also
- Verify a webhook signature — Node, Python, PHP examples.
- Payment statuses — status transition reference.