Webhook Events
This is the annotated catalog of every event Flint can send to your endpoint. For each family it covers what triggers each event, what the data payload carries, and which event to build a given job on. It closes with the sequences you will actually see on the wire for real scenarios: a checkout payment, a refund, a dispute, a subscription renewal.
Two related pages cover the rest:
- To receive events (register endpoints, verify signatures, handle retries), start with Webhooks.
- For the always-current one-line list of every event type, see the live event catalog. This guide annotates that list.
Every event is a fact about one object. The event_type names the object and what happened to it (object.verb), and data carries a snapshot of that object. Switch on event_type, read data, and treat everything else in the envelope as routing and deduplication.
The Event Envelope#
Every merchant event shares one envelope. Only data varies by family.
Here is a complete delivery, headers and body, for order.payment_succeeded:
POST /webhooks/flint HTTP/1.1
Content-Type: application/json
User-Agent: Flint-Webhooks/1.0
X-Flint-Webhook-ID: whev_1kmn0aExample
X-Flint-Event-Type: order.payment_succeeded
X-Flint-Webhook-Endpoint-ID: whep_1kmn0aExample
X-Flint-Signature: t=1783087200,v1=5f3a8c1d9b2e...
webhook-id: whev_1kmn0aExample
webhook-timestamp: 1783087200
webhook-signature: v1,K5oZfzN95Z8sExample=
{
"webhook_event_id": "whev_1kmn0aExample",
"event_type": "order.payment_succeeded",
"payload_version": 1,
"mode": "test",
"merchant_id": "mer_1kmn0aExample",
"created_at": "2026-07-03T14:00:00Z",
"data": {
"order_id": "ord_1kmn0aExample",
"payment_intent_id": "pi_1kmn0aExample",
"status": "paid",
"payment_intent_status": "succeeded",
"paid_money": { "amount": 9900, "currency": "USD" },
"line_items": [
{
"order_line_item_id": "li_1kmn0aExample",
"name": "Annual membership",
"quantity": 1,
"unit_price_money": { "amount": 9900, "currency": "USD" },
"gross_money": { "amount": 9900, "currency": "USD" }
}
]
}
}
webhook_event_idstringStable identifier for this event. Every retry and every manual resend carries the same ID, which makes it your deduplication key. It also arrives in the X-Flint-Webhook-ID header so you can dedupe before parsing the body.
event_typestringThe object.verb name of what happened, such as refund.updated. Switch on this value, and return a 2xx for types you do not handle.
payload_versionnumberThe envelope version, currently 1. New fields can be added without a version bump, so ignore keys you do not recognize.
modestringlive or test. Matches the environment of the key and data that produced the event. Route test events away from production side effects.
merchant_idstringThe merchant the event belongs to.
created_atstringRFC 3339 time the event occurred. This is occurrence time, not delivery time: a retried delivery can arrive more than a day after created_at.
testbooleanPresent and true only on synthetic events sent from the test-events endpoint. Omitted on real events; it is never present as false.
dataobjectThe family-specific snapshot. Shapes are documented per family in the catalog below.
Delivery headers#
| Header | Purpose |
|---|---|
X-Flint-Signature | Signature over the timestamp and raw body: t=<unix>,v1=<hmac>. Two v1 values appear during secret rotation. |
X-Flint-Webhook-ID | Same value as webhook_event_id. Dedupe on it before parsing. |
X-Flint-Event-Type | Same value as event_type. Filter or route before parsing. |
X-Flint-Webhook-Endpoint-ID | The endpoint this delivery targets. Useful when one URL serves several endpoints. |
webhook-id, webhook-timestamp, webhook-signature | The same identity and signature in Standard Webhooks format, so off-the-shelf verification libraries work without custom code. |
Verification itself is covered step by step in Webhooks.
Delivery Semantics That Shape Your Handler#
Three transport facts change how you consume every event on this page:
- Delivery is at least once. The same event can arrive twice. Dedupe on
webhook_event_id. - Success is a
2xxwithin 10 seconds. Anything else counts as a failed attempt. Queue slow work and respond immediately. - Failed deliveries retry up to 9 times over roughly 30 hours. The full schedule is in the delivery contract.
No ordering guarantee
Events fan out to each endpoint independently and retry independently, so a later event can arrive before an earlier one. Never build a state machine that assumes payment_intent.succeeded arrives before order.payment_succeeded. Treat each event as a self-contained fact, and fetch the resource when you need its current state.
Choosing the Right Event#
Most integrations need two or three events, not the whole catalog. Find your job in the left column and subscribe to the event in the middle.
| Job | Build on | Why |
|---|---|---|
| Fulfill an order | order.payment_succeeded | Fires when the order becomes paid on any surface: checkout session, payment link, invoice, or direct API. One subscription covers them all. |
| Know a hosted checkout finished | checkout_session.completed | Carries order_id and payment_intent_id. Redundant if you already fulfill from order.payment_succeeded; pick one. |
| Know a hold was placed | order.payment_authorized | The durable signal that an authorization landed, including after 3D Secure. Use payment_intent.requires_capture for intents without an order. |
| Know captured money settled | order.payment_captured | There is no payment_intent.captured; on bare intents a capture arrives as payment_intent.succeeded. |
| Reconcile money movement | balance_transaction.created | One event per charge, refund, fee, payout, and adjustment that hits your balance. |
| React to refund outcomes | refund.updated | Refunds settle asynchronously; the create response only means accepted. Watch for status: "succeeded" and pair with refund.failed. |
| Respond to disputes | dispute.needs_response | The event with a deadline. dispute.created opens the case; dispute.won and dispute.lost carry the outcome. |
| Grant and revoke subscription access | subscription.payment_succeeded | The minimal entitlement trio: this to provision, subscription.past_due to warn, subscription.canceled to revoke. |
| Track invoice receivables | invoice.paid | Pair with invoice.partially_paid and invoice.voided. Offline payments arrive as invoice.manual_payment_recorded. |
| Know a payout reached your bank | payout.paid | payout.created means scheduled, not arrived. Pair with payout.failed. |
| Keep card-on-file state current | payment_method.saved | Fires when the method is saved and usable, not at form submit. Pair with payment_method.removed. |
Pick one primary event per job and make the handler idempotent. Subscribing to overlapping events for the same job is the most common cause of double fulfillment.
Event Catalog by Family#
Subscribe per endpoint with enabled_events; an empty list means every event. The tables below annotate every current event type, and the live event catalog always reflects the latest list.
Payment Intents#
Payload: a payment_intent object with payment_intent_id, status, and amount_money, plus order_id, customer_id, and cancellation_reason when set. Note the wrapper: this is the only family that nests its object under a key.
{
"payment_intent": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "succeeded",
"amount_money": { "amount": 4200, "currency": "USD" },
"order_id": "ord_1kmn0aExample",
"customer_id": "cus_1kmn0aExample"
}
}
| Event | Fires when |
|---|---|
payment_intent.succeeded | The payment settled, including the capture of a held authorization. |
payment_intent.requires_action | The buyer must act, such as completing 3D Secure authentication. |
payment_intent.requires_capture | The payment was authorized and is waiting for capture. |
payment_intent.payment_failed | A payment attempt failed. |
payment_intent.canceled | The intent was canceled or its authorization expired. cancellation_reason says which. |
There is no payment_intent.expired event; an expired authorization arrives as payment_intent.canceled. If your payments flow through orders, you can usually skip this family and listen at the order level instead.
Orders#
Payload: the order.payment_* events carry order_id, payment_intent_id, the order's public status, and payment_intent_status; authorization events add authorization_status; each event adds the money fields for its transition. order.refunded carries order_id, status, and the refund fields below. The full order.payment_succeeded body is shown in The Event Envelope.
| Event | Fires when | Payload adds |
|---|---|---|
order.payment_succeeded | A payment succeeded and the order is paid. | paid_money, line_items |
order.payment_authorized | A hold was placed and is waiting for capture. | authorized_money, capturable_money, authorization_expires_at |
order.payment_captured | Some or all of a held authorization was captured. | captured_money |
order.payment_authorization_voided | You released a hold without capturing. | released_money |
order.payment_authorization_expired | A hold lapsed before capture. | released_money |
order.refunded | A refund was applied to the order. | refund_id, refund_amount_money, refunded_total_money |
A partial capture fires order.payment_captured without order.payment_succeeded. See Manual Capture for the full authorize-then-capture flow.
Order Fulfillment#
Payload: embeds the public order and fulfillment objects for the change, plus the shipment and package objects where relevant.
| Event | Fires when |
|---|---|
order.fulfillment.status_changed | A fulfillment moved between pending, completed, and canceled. |
order.fulfillment.event.created | A fulfillment event was recorded from a carrier, pickup flow, appointment system, or other fulfillment source. |
order.fulfillment.shipment.created | A shipment was created. |
order.fulfillment.shipment.updated | A shipment changed status. |
order.fulfillment.package.created | A shipment package was created. |
order.fulfillment.package.updated | A shipment package changed status. |
Checkout Sessions#
Payload: thin by design. Fetch the session or the order when you need details.
{
"order_id": "ord_1kmn0aExample",
"payment_intent_id": "pi_1kmn0aExample",
"checkout_session_id": "cs_1kmn0aExample"
}
| Event | Fires when |
|---|---|
checkout_session.completed | A hosted checkout finished and its payment succeeded. |
If you fulfill from order.payment_succeeded, treat this event as the trigger for checkout-specific work such as analytics or post-purchase messaging, not as a second fulfillment trigger. See Checkout Sessions.
Refunds#
Payload: refund_id, status, amount_money, payment_intent_id, and order_id, plus reason and failure_reason when set, per-payment splits in payment_refunds, and per-line-item detail in line_item_allocations.
{
"refund_id": "ref_1kmn0aExample",
"status": "succeeded",
"amount_money": { "amount": 2500, "currency": "USD" },
"payment_intent_id": "pi_1kmn0aExample",
"order_id": "ord_1kmn0aExample",
"payment_refunds": [
{
"payment_intent_id": "pi_1kmn0aExample",
"amount_money": { "amount": 2500, "currency": "USD" },
"refunded_tip_money": { "amount": 0, "currency": "USD" },
"status": "succeeded"
}
],
"line_item_allocations": [
{
"order_line_item_id": "li_1kmn0aExample",
"quantity": 1,
"refunded_money": { "amount": 2500, "currency": "USD" }
}
]
}
| Event | Fires when |
|---|---|
refund.created | A refund was accepted and is processing. |
refund.updated | The refund changed status, such as pending to succeeded. |
refund.failed | The refund could not be completed. failure_reason says why. |
There is no refund.succeeded event. Success is refund.updated with status: "succeeded". See Refunds.
Disputes#
Payload: the full dispute: dispute_id, payment_intent_id, order_id, amount_money, public status, reason, and case_type, plus evidence state such as evidence_due_at, action_required, and evidence_submission_count.
| Event | Fires when |
|---|---|
dispute.created | A payment dispute was opened. |
dispute.needs_response | The dispute needs your evidence and a deadline is running. |
dispute.updated | The dispute changed status or details, such as moving under review. |
dispute.won | The dispute was resolved in your favor. |
dispute.lost | The dispute was resolved against you. |
dispute.closed | The dispute was closed without further action required. |
dispute.prevented | An early-warning case was resolved before becoming a dispute. |
dispute.warning_closed | An early-warning case was closed. |
Act on dispute.needs_response; the others keep your records current.
Subscriptions#
Payload: subscription_id, plan_id, and the plan's line_items; each event adds its own context fields, listed below.
{
"subscription_id": "sub_1kmn0aExample",
"plan_id": "plan_1kmn0aExample",
"line_items": [
{
"name": "Pro plan",
"quantity": 1,
"unit_price_money": { "amount": 1900, "currency": "USD" },
"gross_money": { "amount": 1900, "currency": "USD" }
}
],
"order_id": "ord_1kmn0aExample",
"payment_intent_id": "pi_1kmn0aExample"
}
| Event | Fires when | Payload adds |
|---|---|---|
subscription.created | The subscription was created. | customer_id, status, next_billing_date |
subscription.activated | A trial ended and the subscription became active. | |
subscription.payment_succeeded | A billing cycle charged successfully. | order_id, payment_intent_id |
subscription.payment_failed | A billing attempt failed. Retries follow. | order_id |
subscription.past_due | Billing failed and the subscription is past due while retries run. | status |
subscription.canceled | The subscription was canceled: by request, at period end, or after retries were exhausted. | reason |
subscription.paused | The subscription was paused. | reason |
subscription.resumed | A paused subscription resumed billing. | reason |
Subscriptions without a trial never fire subscription.activated; their first subscription.payment_succeeded is the activation signal. Each successful cycle also creates a regular order, so the cycle's order.payment_succeeded fires too. See Subscription Billing.
Invoices#
Payload: an invoice snapshot: invoice_id, order_id, invoice_number, status, refund_status, amount_due_money, and customer_id when set. Delivery events add the attempt: delivery_kind, delivery_channel, delivery_status, to_email, and error_message on failure.
| Event | Fires when |
|---|---|
invoice.sent | The invoice was sent to the customer. |
invoice.paid | The invoice was paid in full. |
invoice.partially_paid | The invoice received a partial payment. |
invoice.manual_payment_recorded | You recorded an offline payment, such as cash or check. |
invoice.manual_payment_reversed | A recorded offline payment was reversed. |
invoice.refunded | The invoice's payment was fully refunded. |
invoice.partially_refunded | Part of the invoice's payment was refunded. |
invoice.voided | The invoice was voided and can no longer be paid. |
invoice.delivery_succeeded | An invoice email was delivered. |
invoice.delivery_failed | An invoice email could not be delivered. |
See Invoicing.
Customers and Saved Payment Methods#
Payload: customer.created carries customer_id, email, and name; customer.updated carries customer_id, email, and updated_fields. payment_method.saved carries payment_method_id, customer_id, and card display details (card_brand, card_last4, card_exp_month, card_exp_year, plus card_wallet for wallet cards); payment_method.removed carries payment_method_id and customer_id.
| Event | Fires when |
|---|---|
customer.created | A customer record was created. |
customer.updated | A customer record was updated. updated_fields lists what changed. |
payment_method.saved | A payment method finished saving and is ready to charge. |
payment_method.removed | A saved payment method was removed. |
payment_method.saved fires when the save completes, which can be after the buyer has left your page. Treat it as the durable signal that a card on file is usable.
Payouts#
Payload: the payout: payout_id, amount_money, status, method, initiated_by, fee fields, and payout_destination_id when set.
| Event | Fires when |
|---|---|
payout.created | A payout to your bank was created and scheduled. |
payout.updated | The payout changed state, such as moving in transit. |
payout.paid | The payout arrived at your bank. |
payout.failed | The payout failed and funds returned to your balance. |
payout.canceled | The payout was canceled before sending. |
payout.reversed | A completed payout was reversed. |
Payout Destinations and Settings#
Payload: the payout destination, setup session, or settings object that changed, with public status values.
| Event | Fires when |
|---|---|
payout_destination.created | A payout destination, such as a bank account, was added. |
payout_destination.updated | A payout destination was updated. |
payout_destination.disabled | A payout destination was disabled. |
payout_destination.deleted | A payout destination was removed. |
payout_destination_session.completed | A hosted destination setup session completed. |
payout_destination_session.updated | A hosted destination setup session changed state. |
payout_settings.updated | Your payout schedule or settings changed. |
Balances and Capabilities#
Payload: balance.updated carries the full balance (available, pending, held, and instant-available money). balance_transaction.* carries the transaction: balance_transaction_id, type, signed amount_money, fee_money, net_money, status, available_on, and a related_object reference back to its source. capability.updated carries the capability name, its status, and outstanding requirements.
| Event | Fires when |
|---|---|
balance.updated | Your available or pending balance changed. |
balance_transaction.created | A charge, refund, fee, payout, or adjustment was recorded on your balance. |
balance_transaction.updated | A balance transaction changed state, such as pending funds becoming available. |
capability.updated | A merchant capability, such as card payments or payouts, changed status. |
balance_transaction.created is the backbone of reconciliation: one event per ledger entry, each linking back to its source object.
Event Sequences by Scenario#
These sequences show the typical arrival order. Delivery order is not guaranteed, so handle each event independently.
Hosted checkout payment#
A buyer pays on a checkout session or payment link.
buyer pays on the hosted page
-> payment_intent.succeeded the charge settled
-> order.payment_succeeded the order is paid
-> checkout_session.completed the session finished; carries order_id
-> balance_transaction.created the charge hit your balance
Fulfill from order.payment_succeeded or checkout_session.completed, never both.
Manual capture#
You place a hold now and capture later. See Manual Capture.
confirm with manual capture
-> payment_intent.requires_action only if 3D Secure is required
-> payment_intent.requires_capture the hold is on
-> order.payment_authorized carries capturable_money
capture void, or let the hold expire
-> payment_intent.succeeded -> payment_intent.canceled
-> order.payment_captured -> order.payment_authorization_voided
-> order.payment_succeeded or order.payment_authorization_expired
A partial capture fires order.payment_captured without order.payment_succeeded.
Refund#
POST /v1/refunds
-> refund.created status pending
-> refund.updated status succeeded
-> order.refunded order totals updated
-> balance_transaction.created the debit on your balance
if the refund fails
-> refund.failed failure_reason says why
Act on refund.updated reaching succeeded, not on the create response.
Dispute#
card network opens a case
-> dispute.created
-> dispute.needs_response evidence deadline running
you submit evidence
-> dispute.updated case under review
-> dispute.won or dispute.lost
-> dispute.closed
early-warning case
-> dispute.prevented or dispute.warning_closed
Subscription billing#
signup completes
-> subscription.created plus customer.created and payment_method.saved on hosted signup
each successful cycle
-> subscription.payment_succeeded carries the cycle's order_id
-> order.payment_succeeded the cycle's order, like any other order
trial ends
-> subscription.activated
a renewal fails
-> subscription.payment_failed
-> subscription.past_due retries are running
retries recover retries exhaust
-> subscription.payment_succeeded -> subscription.canceled
The entitlement pattern: provision on subscription.payment_succeeded, warn on subscription.past_due, revoke on subscription.canceled.
Payout cycle#
funds scheduled to your bank
-> payout.created -> payout.updated -> payout.paid
-> payout.failed funds return to your balance
after arrival
-> payout.reversed a completed payout was pulled back
Discovering Events at Runtime#
The catalog is machine readable. GET /v1/webhook-event-types returns every event type your endpoints can subscribe to:
curl "https://api.withflintpay.com/v1/webhook-event-types?page_size=100" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"event_type": "balance.updated",
"event_sources": ["merchant", "installed_merchants"]
},
{
"event_type": "balance_transaction.created",
"event_sources": ["merchant", "installed_merchants"]
}
],
"request_id": "2f7c5a9d-6e4b-4f8a-9b73-1d8e4c6a0f25"
}
The event_type values are exactly what enabled_events accepts when you create or update an endpoint. Use this endpoint to validate your configuration in CI, or to build tooling that stays current as new events ship.
Test events are fixtures, not family payloads
POST /v1/webhook-endpoints/{webhook_endpoint_id}/test-events delivers test: true with a minimal fixture payload: the event type, the test flag, and a deterministic resource ID. It does not carry the family shapes documented above. Use test events to prove delivery and signature verification, and use sandbox activity (a real test-mode payment or refund) to exercise payload parsing.
Test events and resends are walked through in Testing.
Partner App Events#
Partner apps are a separate event source with their own endpoints and a thinner envelope: no payload_version, mode, or merchant_id at the top level. partner_app_id identifies your app, and the merchant appears inside data.
{
"webhook_event_id": "whev_1kmn0aExample",
"event_type": "partner_app.install.created",
"partner_app_id": "papp_1kmn0aExample",
"created_at": "2026-07-03T14:00:00Z",
"data": {
"partner_app_id": "papp_1kmn0aExample",
"partner_app_install_id": "pinst_1kmn0aExample",
"merchant_id": "mer_1kmn0aExample",
"status": "active",
"granted_scopes": ["commerce.orders.read"]
}
}
| Event | Fires when |
|---|---|
partner_app.install.created | A merchant installed your app. |
partner_app.install.updated | The install changed state. |
partner_app.install.permissions_updated | The merchant changed your app's granted scopes. |
partner_app.install.revoked | The merchant uninstalled your app. |
partner_app.install.environment_grant.created | The install was granted access to a merchant environment. |
partner_app.install.environment_grant.revoked | An environment grant was revoked. |
Endpoint setup, install tokens, and handling patterns are in Partner App Installs.
Common Mistakes#
- Fulfilling from two overlapping events.
payment_intent.succeeded,order.payment_succeeded, andcheckout_session.completedall fire for one hosted payment. Pick one primary event per job and dedupe onwebhook_event_id. - Assuming arrival order. Deliveries fan out and retry independently, so a later event can arrive first. Handle each event as a self-contained fact.
- Waiting for events that do not exist. There is no
payment_intent.captured(a capture arrives aspayment_intent.succeeded) and norefund.succeeded(success isrefund.updatedwithstatus: "succeeded"). - Treating the payload as current state.
datais a snapshot fromcreated_at, and a retry can deliver it more than a day later. Fetch the resource before acting on fields that may have moved. - Rejecting unknown event types. New event types ship over time, and endpoints subscribed to all events receive them immediately. Return a
2xxfor types you do not handle; an error response burns retries on events you never wanted.
Next Steps#
- Webhooks: register endpoints, verify signatures, and process retries safely.
- Live event catalog: the always-current one-line list of every event type.
- Testing: test events, resends, and sandbox validation.
- Statuses & Lifecycles: the status values these events report.
- Manual Capture: the authorize-then-capture flow behind the order authorization events.
- Subscription Billing: plans, trials, and the billing lifecycle behind the subscription events.
