> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.fluveo.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.fluveo.com/_mcp/server.

# Webhook event types

This page lists event types accepted by merchant-public WebhookEndpoints.
Availability still depends on whether the source product can emit the event.

## How to read this catalog

Each type has an implementation status inside the platform:

| Status             | Meaning                                                                 |
| ------------------ | ----------------------------------------------------------------------- |
| **Implemented**    | The internal projection is emitted by normal payment flows.             |
| **Flow-dependent** | Emitted internally only when a payment reaches the corresponding state. |
| **Dormant**        | Projection/routing code exists, but its source product is unavailable.  |

## Internal routing

Each merchant-public endpoint has an `enabled_events` allow-list. Use an exact
type below or the `*` wildcard.

## Stripe-shaped types (19)

| Type                                       | When it fires                                                                 | `data.object` | Status          |
| ------------------------------------------ | ----------------------------------------------------------------------------- | ------------- | --------------- |
| `payment_intent.succeeded`                 | a payment is authorized and captured                                          | PaymentIntent | **Implemented** |
| `payment_intent.payment_failed`            | a payment attempt is declined or errors                                       | PaymentIntent | **Implemented** |
| `payment_intent.requires_action`           | a payment needs customer action (e.g. 3DS)                                    | PaymentIntent | **Implemented** |
| `charge.refunded`                          | a refund succeeds                                                             | Charge        | **Implemented** |
| `payment_intent.canceled`                  | a payment is canceled before capture                                          | PaymentIntent | Flow-dependent  |
| `payment_intent.amount_capturable_updated` | an authorization is held for later capture (manual capture)                   | PaymentIntent | Flow-dependent  |
| `payment_intent.processing`                | a payment enters async processing — bank debits; cards do not rest here       | PaymentIntent | Flow-dependent  |
| `refund.failed`                            | a refund fails at the connector                                               | Refund        | Flow-dependent  |
| `charge.succeeded`                         | a card charge succeeds (or is authorized, for manual capture)                 | Charge        | Flow-dependent  |
| `charge.captured`                          | a manual-capture charge is captured                                           | Charge        | Flow-dependent  |
| `charge.updated`                           | an automatic-capture charge gets its balance transaction                      | Charge        | Flow-dependent  |
| `charge.failed`                            | a charge attempt is declined                                                  | Charge        | Flow-dependent  |
| `charge.dispute.created`                   | a dispute is opened                                                           | Dispute       | Dormant         |
| `charge.dispute.updated`                   | a dispute is challenged or updated                                            | Dispute       | Dormant         |
| `charge.dispute.closed`                    | a dispute is resolved (won, lost, or expired)                                 | Dispute       | Dormant         |
| `charge.dispute.funds_withdrawn`           | Stripe withdraws a disputed amount (US Stripe Connect only)                   | Dispute       | Dormant         |
| `charge.dispute.funds_reinstated`          | Stripe returns a disputed amount after a won dispute (US Stripe Connect only) | Dispute       | Dormant         |
| `mandate.updated`                          | a mandate becomes active or is revoked                                        | Mandate       | Dormant         |
| `payout.paid`                              | a payout completes                                                            | Payout        | Dormant         |
| `payout.failed`                            | a payout fails                                                                | Payout        | Dormant         |
| `payout.created`                           | a payout is created and processing                                            | Payout        | Dormant         |
| `payout.canceled`                          | a payout is canceled                                                          | Payout        | Dormant         |

### Charge events

`charge.succeeded`, `charge.captured`, `charge.failed` and `charge.updated` are published by the
payment event of the same moment. Their `id` is that event's id plus a suffix
(`_chs`, `_chc`, `_chf`, `_chu`) and never changes, so deduplicate on it.
`charge.succeeded` has `balance_transaction: null`, as at Stripe; for an automatic
capture, `charge.updated` follows with the `txn_` and `previous_attributes`
`{balance_transaction: null}`. `charge.pending` is never sent: no Fluveo payment
creates a pending Charge. The Charge is
the `GET /v1/charges/{id}` body without the shopper's name, email, phone or
address. `amount_captured`, `captured`, `amount_refunded` and `refunded` keep
their values from when the event happened.
These four types are sent for US Stripe Connect stores only; a BR store cannot
subscribe to them. On a US Stripe Connect store every refund also sends
`refund.created` once, when it is created. Its Refund keeps the processor's
status at that moment, so it can say `succeeded` while `GET /v1/refunds/{id}`
still reads `pending` until Fluveo's books confirm it. A later status change of the refund sends
`charge.refund.updated` (and `refund.updated` when it is not a failure; a failure sends `refund.failed`),
with `previous_attributes`. A metadata update (`POST /v1/refunds/{id}`) sends `charge.refund.updated` and
`refund.updated` with `previous_attributes` `{metadata: ...}`.

### A few types collapse several states

Some Stripe types intentionally cover more than one underlying platform state —
branch on a field of `data.object`, not on a finer-grained type:

* **`payment_intent.succeeded`** covers both a one-step sale and a captured
  authorization.
* **`charge.dispute.closed`** covers a dispute that was **won, lost, or expired** —
  read the dispute's `status` to tell which.
* **`mandate.updated`** covers a mandate becoming **active** or being **revoked** —
  read the mandate's `status`.

### Example bodies — implemented internal projections

The `data.object` is always a clean public shape. See [delivery safety](/webhooks#delivery-safety). Abbreviated examples:

```json
// payment_intent.succeeded
{ "type": "payment_intent.succeeded",
  "data": { "object": {
    "object": "payment_intent", "id": "pi_…", "status": "succeeded",
    "amount": 4242, "currency": "usd", "latest_charge": "ch_…" } } }

// payment_intent.payment_failed
{ "type": "payment_intent.payment_failed",
  "data": { "object": {
    "object": "payment_intent", "id": "pi_…", "status": "requires_payment_method",
    "last_payment_error": { "code": "card_declined", "decline_code": "generic_decline",
      "message": "Your card was declined." } } } }

// payment_intent.requires_action
{ "type": "payment_intent.requires_action",
  "data": { "object": {
    "object": "payment_intent", "id": "pi_…", "status": "requires_action",
    "next_action": null } } }

// charge.refunded — data.object is a Charge, not a Refund
{ "type": "charge.refunded",
  "data": { "object": {
    "object": "charge", "id": "ch_…", "amount": 4242, "amount_refunded": 4242,
    "refunded": true, "payment_intent": "pi_…" } } }
```

Payment events never carry `next_action`; retrieve the PaymentIntent to read it.

For Dormant types, the example body will be published when the source resource
ships; the `data.object` will be the public Stripe shape of that resource.

> These shapes are internal implementation evidence. See the
> [webhook migration boundary](/guides/migrate-from-stripe#webhook-differences-from-stripe)
> before planning a public integration.

## Platform-native types (9 `hs.*`)

Internal projections with no Stripe equivalent use a stable `hs.<snake_case>`
name rather than being dropped. The three `hs.payment_*` types carry the same
PaymentIntent object as `payment_intent.*` (`object: "payment_intent"`, a `pi_`
id, no shopper fields); each of the other six carries a named public object too:
on webhook endpoints, never the stored customer's personal data (the legacy
profile URL is never sent to; events go only to webhook endpoints). This naming
is not a merchant-public delivery
promise.

| Type                                | When it fires                        | `data.object`                                                  | Status         |
| ----------------------------------- | ------------------------------------ | -------------------------------------------------------------- | -------------- |
| `hs.payment_cancelled_post_capture` | a payment is cancelled after capture | PaymentIntent (`object: "payment_intent"`)                     | Flow-dependent |
| `hs.payment_partially_authorized`   | a payment is partially authorized    | PaymentIntent (`object: "payment_intent"`)                     | Flow-dependent |
| `hs.payment_expired`                | a payment expires before completion  | PaymentIntent (`object: "payment_intent"`)                     | Flow-dependent |
| `hs.dispute_accepted`               | a dispute is accepted                | Dispute (`object: "dispute"`) with the charge registry's `ch_` | Dormant        |
| `hs.dispute_cancelled`              | a dispute is cancelled               | Dispute (`object: "dispute"`) with the charge registry's `ch_` | Dormant        |
| `hs.invoice_paid`                   | an invoice is paid                   | Invoice (`object: "invoice"`)                                  | Dormant        |
| `hs.payout_initiated`               | a payout is initiated                | Payout (`object: "payout"`)                                    | Dormant        |
| `hs.payout_expired`                 | a payout expires                     | Payout (`object: "payout"`)                                    | Dormant        |
| `hs.payout_reversed`                | a payout is reversed                 | Payout (`object: "payout"`)                                    | Dormant        |

The Invoice carries exactly `object`, `id`, `subscription`, `customer`,
`payment_intent`, `amount_due`, `currency` and `status` (`draft`, `open`, `paid`
or `void`); never the subscription's payment or billing details.

## The internal `*` wildcard

The platform routing model recognizes `*` as a merchant-public subscription to
every event type that the merchant and mode are allowed to receive.

## Hidden from the public API

Each item below is hidden because it does not follow the Stripe parity ADR (ADR 0004 as amended). The [hidden public `/v1` registry](https://github.com/fluveohq/fluveo/blob/main/tests/api-contract/stripe/hidden-public-v1.json) lists every
hidden item. Public `/v1` answers a hidden operation as an unknown URL, refuses a hidden request
parameter or value with Stripe's `invalid_request_error`, and omits a hidden response key.

* the query parameter `object` of `GET /v1/events` (HIDE0-047)
* the query parameter `payment_intent` of `GET /v1/events` (HIDE0-048)