Webhook event catalog
Audience: anyone receiving webhooks from Movere. Platforms on Movere 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>. Non-2xx responses retry, with
up to eight attempts total. Retry delays are 60s, 120s, 240s, 480s,
960s, 1920s, and 3600s, plus retry-loop scheduling time. Transport
failures also retry; the last failure dead-letters the delivery. 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": { "payment_id": "pay_example", "state": "captured" }
}
created is RFC3339 UTC with exactly three fractional-second digits.
The merchant is identified by a nested merchant object — one
endpoint receives enabled events for connected tenants with active
grants. Ordinary payment fanout currently covers Live only. Test
payment events do not reach platform destinations. This Connect
envelope has no environment or livemode field; grant-revocation
events use a separate direct delivery path after revocation.
Merchant destinations
{
"id": "evt_01HXX…",
"type": "payment.captured",
"occurred_at": "2026-06-10T12:34:56Z",
"merchant_id": "merch_lodge_001",
"livemode": true,
"data": { "payment_id": "pay_example", "state": "captured" }
}
livemode is true for Live merchant events and false for Test.
Merchant destinations are selected within the event's environment.
The envelopes differ: 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 for an event 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 | Cumulative refund snapshot below |
payment.partially_refunded |
A partial refund leaves a remaining balance | Cumulative refund snapshot below |
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.
Refund snapshots
Both refund event types carry these fields in data. All amounts are
integer minor units in the payment's currency (1000 = GBP 10.00).
| Field | Meaning |
|---|---|
payment_id |
Original Mooov payment ID; correlate within the envelope's merchant |
state |
partially_refunded or refunded at the time of this snapshot |
refunded_amount |
Cumulative refunded amount for this payment, not this operation's amount |
amount_refunded |
Compatibility alias with the same value as refunded_amount |
captured_amount |
Captured amount against which the refunds apply |
remaining_amount |
Captured amount minus cumulative refunded amount, never below zero |
currency |
Payment currency |
For example, after two GBP 10.00 partial refunds against a GBP 50.00 payment, the second notification contains:
{
"id": "evt_example_second_partial",
"type": "payment.partially_refunded",
"created": "2026-10-07T12:00:00.000Z",
"merchant": { "id": "merch_example", "entity_id": "mooov3" },
"data": {
"payment_id": "pay_example",
"state": "partially_refunded",
"currency": "GBP",
"refunded_amount": 2000,
"amount_refunded": 2000,
"captured_amount": 5000,
"remaining_amount": 3000
}
}
Refunding the remaining GBP 30.00 produces payment.refunded with
state: "refunded", both cumulative fields set to 5000, and
remaining_amount: 0. The example omits unrelated optional fields.
The legacy data.amount field is source-dependent: API/portal-origin
events use the current refund operation amount, while Stripe-origin
events use the captured amount. Do not use amount to calculate
refund totals. The refund API response also differs from a webhook:
its refunded_amount is the current operation amount, while its
refunded_total is cumulative. See the API reference.
Successive partial refunds have distinct event IDs; redelivery of the
same event retains its ID. An API response and an API-origin or
provider-origin event can all describe the same refunded funds, and
different source events can have different IDs. Dedupe delivery by
event.id, then merge cumulative totals rather than adding them.
For each merchant/payment, atomically retain the greater of the stored
and incoming cumulative totals. Derive the refund status from that
total and the captured amount. A late partial snapshot or delayed
payment-success event must not reduce the total or undo a full refund.
Apply the same merge to API responses using refunded_total; an API
response can arrive after a newer webhook. Update the ledger record
used by your UI as well as any internal payment-attempt record.
Older stored events are replayed as originally produced and may lack
the new snapshot fields. For legacy Stripe-origin events,
amount_refunded supplies the cumulative total; do not infer a full
refund solely from a legacy event's type. If neither cumulative field
is available, reconcile before changing totals. Subscribe your Live
Connect destination to both refund event types; Test payment
events are not fanned out to platform destinations.
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 or incomplete_expired |
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 the 10s request timeout) and do the heavy work async — a slow handler is indistinguishable from a dead one and triggers retries. - Dedupe retries on
event.id. Different sources can report the same business change under different IDs; refund snapshots must also be merged by cumulative total as described above. - 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.