Webhook event catalog
Audience: anyone receiving webhooks from Mooov. Platforms on Mooov Connect and merchants with their own webhook destination receive slightly different envelopes — both are documented here.
Signature verification, retry schedule, and dedupe guidance live in
§6 of the Connect protocol and apply to both
audiences unchanged: X-Mooov-Signature: t=<unix>,v1=<hex> where the
HMAC-SHA256 input is <t>.<raw body>, retries on non-2xx with
exponential backoff (1s, 5s, 30s, 5m, 30m, 2h, 12h), dedupe on
X-Mooov-Delivery or event.id.
1. Two envelopes
Connect platforms
{
"id": "evt_01HXX…",
"type": "payment.succeeded",
"created": "2026-06-10T12:34:56.000Z",
"merchant": { "id": "merch_lodge_001", "entity_id": "mooov3" },
"data": { … }
}
created is RFC3339 UTC with exactly three fractional-second digits.
The merchant is identified by a nested merchant object — one
endpoint receives events for all your connected tenants.
Merchant destinations
{
"id": "evt_01HXX…",
"type": "payment.captured",
"occurred_at": "2026-06-10T12:34:56Z",
"merchant_id": "merch_lodge_001",
"data": { … }
}
Note the two deliberate differences: merchant webhooks use the
internal event names (payment.captured) and a flat
merchant_id + occurred_at, while Connect webhooks use the mapped
public names (payment.succeeded) and merchant + created. The
data payload is the same in both lanes.
2. Event name mapping
| Merchant destinations (internal) | Connect platforms (public) |
|---|---|
payment.captured |
payment.succeeded |
payment.failed |
payment.failed |
payment.refunded |
payment.refunded |
payment.partially_refunded |
payment.partially_refunded |
payment.disputed |
payment.disputed |
payment.authorized |
— (not delivered to Connect) |
payment.voided |
— (not delivered to Connect) |
subscription.activated |
subscription.activated |
subscription.updated |
subscription.updated |
subscription.canceled |
subscription.canceled |
subscription.invoice_paid |
subscription.invoice_paid |
subscription.invoice_failed |
subscription.invoice_failed |
| — | grant.revoked (Connect-only; fired on integrator- and operator-initiated grant revokes) |
3. Payment events
All payment events share a data payload shaped like this. Fields
are omitted when empty, never emitted as null:
"data": {
"payment_id": "pay_…",
"state": "captured",
"amount": 4200,
"currency": "GBP",
"metadata": { "your_key": "your_value" },
"customer_ref": "mbr_…",
"payment_method_id": "pm_…"
}
| Event (public name) | Fires when | Extra data fields |
|---|---|---|
payment.succeeded |
A payment is captured — synchronous server-flow confirm, async hosted-checkout completion, or a subscription cycle's invoice payment | — |
payment.failed |
A charge attempt fails terminally | failure_reason, failure_code, failure_category |
payment.refunded |
The full captured amount has been refunded | — |
payment.partially_refunded |
A partial refund leaves a remaining balance | — |
payment.disputed |
The cardholder opened a dispute/chargeback | — |
failure_code is the typed code your integration should branch on —
e.g. insufficient_funds, card_declined, account_invalid (the
merchant's PSP connection broke; see §5.2 of the
Connect protocol for the repair flow).
Merchant-destination extras: payment.voided includes void_reason
in data; payment.authorized fires when a delayed-capture payment
authorizes. Neither is fanned out to Connect platforms.
4. Subscription events
Fired when a connected account runs Stripe Subscriptions. The five
types, the dual-emit contract (each paid cycle also produces a
payment.succeeded with its own refundable payment_id), and the
full subscription.invoice_paid payload example are documented in
§6 of the Connect protocol.
| Event | Fires when |
|---|---|
subscription.activated |
First customer.subscription.created on the account |
subscription.updated |
Subscription changed (excluding transitions to canceled) |
subscription.canceled |
Subscription deleted or its status became canceled |
subscription.invoice_paid |
A billing cycle's invoice was paid (dual-emitted with payment.succeeded) |
subscription.invoice_failed |
A billing cycle's invoice failed (dual-emitted with payment.failed) |
5. Handler checklist
- Verify the signature before parsing the body; compare in constant time.
- Respond
2xxfast (under 10s) and do the heavy work async — a slow handler is indistinguishable from a dead one and triggers retries. - Dedupe on
event.id: retries and dual-source events reuse it. - Branch on
typeand ignore unknown types — new event types are an additive change under the versioning policy and may appear without notice. - Treat field absence as "not applicable", not an error: empty fields
are omitted, never
null.
Test all of this against the sandbox before going live.