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.paid:

HTTP
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.paid
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=
JSON
{
  "webhook_event_id": "whev_1kmn0aExample",
  "event_type": "order.paid",
  "payload_version": 1,
  "mode": "test",
  "merchant_id": "mer_1kmn0aExample",
  "created_at": "2026-07-03T14:00:00Z",
  "data": {
    "order_id": "ord_1kmn0aExample",
    "status": "closed",
    "payment_status": "paid",
    "total_money": { "amount": 9900, "currency": "USD" },
    "paid_money": { "amount": 9900, "currency": "USD" },
    "outstanding_money": { "amount": 0, "currency": "USD" },
    "order_payment_intent_ids": ["pi_1kmn0aExample"],
    "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_idstring

Stable 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_typestring

The 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_versionnumber

The envelope version, currently 1. New fields can be added without a version bump, so ignore keys you do not recognize.

modestring

live or test. Matches the environment of the key and data that produced the event. Route test events away from production side effects.

merchant_idstring

The merchant the event belongs to.

created_atstring

RFC 3339 time the event occurred. This is occurrence time, not delivery time: a retried delivery can arrive more than a day after created_at.

testboolean

Present and true only on synthetic events sent from the test-events endpoint. Omitted on real events; it is never present as false.

dataobject

The family-specific snapshot. Shapes are documented per family in the catalog below.

Delivery headers#

HeaderPurpose
X-Flint-SignatureSignature over the timestamp and raw body: t=<unix>,v1=<hmac>. Two v1 values appear during secret rotation.
X-Flint-Webhook-IDSame value as webhook_event_id. Dedupe on it before parsing.
X-Flint-Event-TypeSame value as event_type. Filter or route before parsing.
X-Flint-Webhook-Endpoint-IDThe endpoint this delivery targets. Useful when one URL serves several endpoints.
webhook-id, webhook-timestamp, webhook-signatureThe 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 2xx within 10 seconds. Anything else counts as a failed attempt. Queue slow work and respond immediately.
  • Failed deliveries retry up to 9 total attempts spanning about three days (~71 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.paid. 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.

JobBuild onWhy
Fulfill an orderorder.paidFires 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 finishedcheckout_session.completedCarries order_id and payment_intent_id. Redundant if you already fulfill from order.paid; pick one.
Know a hold was placedorder.payment_authorizedThe durable signal that an authorization landed, including after 3D Secure. Use payment_intent.requires_capture for intents without an order.
Know captured money settledorder.payment_capturedThere is no payment_intent.captured; on bare intents a capture arrives as payment_intent.succeeded.
Reconcile money movementbalance_transaction.createdOne event per charge, refund, fee, payout, and adjustment that hits your balance.
React to refund outcomesrefund.updatedRefunds settle asynchronously; the create response only means accepted. Watch for status: "succeeded" and pair with refund.failed.
Respond to disputesdispute.needs_responseThe event with a deadline. dispute.created opens the case; dispute.won and dispute.lost carry the outcome.
Grant and revoke subscription accesssubscription.payment_succeededThe minimal entitlement trio: this to provision, subscription.past_due to warn, subscription.canceled to revoke.
Track invoice receivablesinvoice.paidPair with invoice.partially_paid and invoice.voided. Offline payments arrive as invoice.manual_payment_recorded.
Know a payout reached your bankpayout.paidpayout.created means scheduled, not arrived. Pair with payout.failed.
Keep card-on-file state currentpayment_method.savedFires when the method is saved and usable, not at form submit. Pair with payment_method.failed and payment_method.removed.
React to verification and readiness changesmerchant.readiness.updatedA fetch trigger, not a change notification: on receipt, read GET /v1/onboarding/state or the readiness endpoint. Use capability.updated for per-capability changes.

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. The payload carries no fee: read processing_fee_money from GET /v1/payment-intents/{payment_intent_id}, or take it from the payment's balance_transaction.created event. See Processing Fees.

JSON
{
  "payment_intent": {
    "payment_intent_id": "pi_1kmn0aExample",
    "status": "succeeded",
    "amount_money": { "amount": 4200, "currency": "USD" },
    "order_id": "ord_1kmn0aExample",
    "customer_id": "cus_1kmn0aExample"
  }
}
EventFires when
payment_intent.succeededThe payment settled, including the capture of a held authorization.
payment_intent.requires_actionThe buyer must act, such as completing 3D Secure authentication.
payment_intent.processingThe payment was submitted and is waiting for a final outcome. Keep the order unfulfilled.
payment_intent.requires_captureThe payment was authorized and is waiting for capture.
payment_intent.payment_failedA payment attempt failed.
payment_intent.canceledThe 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: order.paid and order.partially_paid carry order_id, workflow status, payment_status, total_money, paid_money, outstanding_money, order_payment_intent_ids, and line items. The order.payment_* authorization events identify one payment_intent_id and carry its payment_intent_status; authorization events also add authorization_status and the transition-specific money. order.refunded carries order_id, workflow status, and the refund fields below. The full order.paid body is shown in The Event Envelope.

EventFires whenPayload adds
order.partially_paidThe order records its first partial settlement.payment_status, paid_money, outstanding_money, order_payment_intent_ids, line_items
order.paidThe order reaches full settlement and closes.payment_status, paid_money, outstanding_money, order_payment_intent_ids, line_items
order.payment_authorizedA hold was placed and is waiting for capture.authorized_money, capturable_money, authorization_expires_at
order.payment_capturedSome or all of a held authorization was captured.captured_money
order.payment_authorization_canceledYou released a hold without capturing.released_money
order.payment_authorization_expiredA hold lapsed before capture.released_money
order.refundedA refund was applied to the order.refund_id, refund_amount_money, refunded_total_money

A partial capture fires order.payment_captured without order.paid. 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.

EventFires when
order.fulfillment.status_changedA fulfillment moved between pending, completed, and canceled.
order.fulfillment.event.createdA fulfillment event was recorded from a carrier, pickup flow, appointment system, or other fulfillment source.
order.fulfillment.shipment.createdA shipment was created.
order.fulfillment.shipment.updatedA shipment changed status.
order.fulfillment.package.createdA shipment package was created.
order.fulfillment.package.updatedA shipment package changed status.

Inventory#

Payload: embeds the public inventory level, reservation, or return receipt for the change. See the Inventory API for the quantity model these events report.

Events fire for every qualifying change, including ones your own synchronous command caused. Deduplicate by event ID rather than trying to tell self-initiated changes from Flint-initiated ones.

EventFires when
inventory.level.updatedAn inventory level revision advanced. Closely spaced changes may coalesce into one event carrying the latest revision, so intermediate states are not individually delivered.
inventory.return_receipt.createdReturned merchandise was received into Inventory through a typed return disposition.
inventory.shortage.detectedA level has more protected claims than sellable physical stock.
inventory.action_requiredA claim needs a safe merchant action before commerce can continue.
inventory.reservation.createdA reservation was created with exact location assignments.
inventory.reservation.committedHeld quantity became committed.
inventory.reservation.consumedFulfillment consumed committed physical stock.
inventory.reservation.releasedHeld or committed quantity was released.
inventory.reservation.hold_expiredA merchant-owned hold reached its deadline.
inventory.reservation.at_riskA held or committed claim became insufficiently protected.
inventory.reservation.closedA reservation has no active held or committed quantity left.

One reservation expiry can emit up to three events together: inventory.reservation.released, inventory.reservation.hold_expired, and, when no active quantity remains, inventory.reservation.closed. Every event for one transition references the same resulting reservation revision, and delivery order is never significant.

High-volume availability checks and routing previews do not emit events, because they hold no stock.

Two related events live on the order rather than on inventory:

EventFires when
order.inventory_exception.createdAn order has an inventory failure that requires operator action.
order.inventory_exception.resolvedAn order inventory exception was resolved.

Checkout Sessions#

Payload: thin by design. Fetch the session or the order when you need details.

checkout_session.completed sends the identifiers of the finished checkout:

JSON
{
  "order_id": "ord_1kmn0aExample",
  "payment_intent_id": "pi_1kmn0aExample",
  "checkout_session_id": "cs_1kmn0aExample"
}

The three terminal events share one shape: checkout_session_id, status, reason, and resource_updated_at, plus order_id, invoice_id, or payment_link_id when the session belongs to one. An expired session adds expired_at, and a replaced session adds superseding_checkout_session_id pointing at the session that took over:

JSON
{
  "checkout_session_id": "cs_1kmn0aExample",
  "status": "invalidated",
  "reason": "superseded",
  "resource_updated_at": "2026-07-14T17:00:00Z",
  "order_id": "ord_1kmn0aExample",
  "superseding_checkout_session_id": "cs_1kmn0aSuccessor"
}
EventFires whenreason values
checkout_session.completedA hosted checkout finished and its payment succeeded.Not sent
checkout_session.closedYou closed the session through the API before payment.api
checkout_session.expiredThe session passed expired_at without payment.expired
checkout_session.invalidatedThe session stopped matching its source, or a replacement superseded it.order_mutated, superseded, invoice_paid_elsewhere, invoice_voided, invoice_uncollectible

If you fulfill from order.paid, treat checkout_session.completed as the trigger for checkout-specific work such as analytics or post-purchase messaging, not as a second fulfillment trigger. When an invalidated event carries superseding_checkout_session_id, the buyer has a newer session; send them its URL rather than treating the sale as lost. 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. A refund raised by a Return resolution adds return_id and return_resolution_id; refunds you create directly omit both.

JSON
{
  "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" }
    }
  ]
}
EventFires when
refund.createdA refund was accepted and is processing.
refund.updatedThe refund changed status, such as pending to succeeded.
refund.failedThe refund could not be completed. failure_reason says why.

There is no refund.succeeded event. Success is refund.updated with status: "succeeded". See Refunds.

Returns#

Payload: the public snapshot of the object named in the event type: a Return, ReturnReceipt, ReturnInspection, ReturnDisposition, or ReturnResolution. supported_actions is omitted, because a delivery carries no caller authority to evaluate those actions against. Read the resource when you need the actions your key can invoke.

One event is published per resource revision, keyed on the event type, the resource ID, and that revision, so a retried transition converges on one logical event. Return revisions are not coalesced the way inventory levels are: every revision that changes a Return gets its own event.

The Return itself:

EventFires when
return.createdA Return was created.
return.updatedA Return or one of its line-level progress projections changed.
return.decision_recordedThe merchant or policy automation recorded line-level Return decisions.
return.canceledA Return was canceled.
return.completedA Return completed after every required merchandise and buyer-value obligation cleared.
return.reopenedA completed Return was reopened for additional work.

Physical facts recorded against the merchandise:

EventFires when
return_receipt.createdA Return receipt observation was recorded.
return_receipt.verifiedAn unmatched Return receipt line was verified against the expected merchandise.
return_receipt.supersededA Return receipt observation was superseded by an auditable correction.
return_inspection.createdA Return inspection observation was recorded.
return_inspection.acceptance_decidedAcceptance was decided for a Return inspection observation.
return_inspection.supersededA Return inspection observation was superseded by an auditable correction.
return_disposition.createdA merchandise disposition was recorded for a Return.
return_disposition.updatedA Return disposition changed execution state.

Buyer value:

EventFires when
return_resolution.createdA proposed buyer-value resolution was created for a Return.
return_resolution.updatedA Return resolution changed proposal, gate, effect, or fulfillment state.

A superseded receipt or inspection is an auditable correction, not a deletion. The superseded observation stays readable and the correction arrives as a new observation, so treat .superseded as a signal to re-read rather than to discard what you stored.

Resolutions settle through the families above: a refund resolution produces ordinary refund.* and order.refunded events, and a resolution the buyer pays produces ordinary payment_intent.* events. A refund raised by a resolution carries return_id and return_resolution_id, so a finance handler can attribute it without keeping its own map. Refunds you create directly omit both.

Returns maps these events to buyer portal, POS, warehouse, and finance integrations.

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.

EventFires when
dispute.createdA payment dispute was opened.
dispute.needs_responseThe dispute needs your evidence and a deadline is running.
dispute.updatedThe dispute changed status or details, such as moving under review.
dispute.wonThe dispute was resolved in your favor.
dispute.lostThe dispute was resolved against you.
dispute.closedThe dispute was closed without further action required.
dispute.preventedAn early-warning case was resolved before becoming a dispute.
dispute.warning_closedAn 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.

JSON
{
  "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"
}
EventFires whenPayload adds
subscription.createdThe subscription was created.customer_id, status, next_billing_date
subscription.activatedA trial ended and the subscription became active.
subscription.payment_succeededA billing cycle charged successfully.order_id, payment_intent_id
subscription.payment_failedA billing attempt failed. Retries follow.order_id
subscription.past_dueBilling failed and the subscription is past due while retries run.status
subscription.canceledThe subscription was canceled: by request, at period end, or after retries were exhausted.reason
subscription.pausedThe subscription was paused.reason
subscription.resumedA 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.paid fires too. See Subscription Billing.

Invoices#

Payload: an invoice snapshot: invoice_id, order_id, invoice_number, status, refund_status, outstanding_money, and customer_id when set. Delivery events add the attempt: delivery_kind, delivery_channel, delivery_status, to_email, and error_message on failure.

EventFires when
invoice.sentThe invoice was sent to the customer.
invoice.paidThe invoice was paid in full.
invoice.partially_paidThe invoice received a partial payment.
invoice.manual_payment_recordedYou recorded an offline payment, such as cash or check.
invoice.manual_payment_reversedA recorded offline payment was reversed.
invoice.refundedThe invoice's payment was fully refunded.
invoice.partially_refundedPart of the invoice's payment was refunded.
invoice.voidedThe invoice was voided and can no longer be paid.
invoice.delivery_succeededAn invoice email was delivered.
invoice.delivery_failedAn 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. payment_method.failed reports a save that did not complete, so no card on file is usable.

EventFires when
customer.createdA customer record was created.
customer.updatedA customer record was updated. updated_fields lists what changed.
payment_method.savedA payment method finished saving and is ready to charge.
payment_method.failedSaving a payment method failed, so no card on file became usable.
payment_method.removedA 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.

Customer Deletion#

Payload: the deletion request: customer_deletion_request_id, customer_id, status, retention_policy, requested_at, and resolved_at once the request is resolved.

EventFires when
customer.deletion_requestedA buyer asked you to delete their account data. status is pending_review.
customer.deletion_completedThe request was approved and the customer was anonymized.
customer.deletion_rejectedThe request was declined.

A buyer raises the request from their account, and someone on your side approves or rejects it. Subscribe to customer.deletion_requested if that decision needs to reach a person or a ticket queue rather than sitting in the dashboard.

Deletion anonymizes the customer. It does not erase commerce records Flint is required to keep, which is what retention_policy: "retain_required_commerce_records" reports. Orders, payments, and refunds survive with the buyer's identity removed. If you hold your own copy of the buyer's data, customer.deletion_completed is your signal to delete it.

See customer accounts.

Payouts#

Payload: the payout: payout_id, amount_money, status, method, initiated_by, fee fields, and payout_destination_id when set.

EventFires when
payout.createdA payout to your bank was created and scheduled.
payout.updatedThe payout changed state, such as moving in transit.
payout.paidThe payout arrived at your bank.
payout.failedThe payout failed and funds returned to your balance.
payout.canceledThe payout was canceled before sending.
payout.reversedA completed payout was reversed.

Payout Destinations and Settings#

Payload: the payout destination or settings object that changed, with public status values.

EventFires when
payout_destination.createdA payout destination, such as a bank account, was added.
payout_destination.updatedA payout destination was updated.
payout_destination.disabledA payout destination was disabled.
payout_destination.deletedA payout destination was removed.
payout_settings.updatedYour payout schedule or settings changed.

Balances and Capabilities#

Payload: balance.updated carries the full public balance (available, pending, held, reserved, and unavailable money). balance_transaction.* carries the transaction: balance_transaction_id, type, signed amount_money, fee_money, net_money, status, and a related_object reference back to its source. available_at is set only once the funds have a date they become available, so a pending transaction does not carry it. fee_money is Flint's own fee on that movement. On a payment it matches the payment's processing_fee_money (Processing Fees); on movement types that carry no Flint fee, including refunds, disputes, payouts, and Flint billing collections, it is zero. capability.updated carries the capability name, its status, and outstanding requirements.

EventFires when
balance.updatedYour available or pending balance changed.
balance_transaction.createdA movement was recorded on your balance: a charge, refund, dispute, fee, payout, Flint billing collection, or adjustment.
balance_transaction.updatedA balance transaction changed state, such as pending funds becoming available.
capability.updatedA 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.

Flint Billing#

Flint billing is what you owe Flint, not what your customers owe you. Its identifiers carry the merchant_billing_ prefix.

Payload: merchant_billing_balance.updated carries merchant_billing_balance_id, outstanding_money, available_credit_money, and the snapshot's observed_at timestamp. Both invoice events carry the invoice Flint issued to you, identical to what GET /v1/merchant-subscription-invoices/{merchant_subscription_invoice_id} returns: merchant_subscription_invoice_id, invoice_number, status, the four money fields, the period and due timestamps, and lines.

EventFires when
merchant_billing_balance.updatedWhat you owe Flint, or the account credit you hold, changed.
merchant_subscription_invoice.issuedFlint issued you a subscription invoice.
merchant_subscription_invoice.updatedOne of Flint's invoices to you changed status.

When Flint collects what you owe out of your Flint balance, that collection also fires balance_transaction.created with type merchant_billing_payment, and a reversed collection fires one with type merchant_billing_payment_reversal. The reversal's related_balance_transaction_ids holds the ID of the collection it reverses. See Flint Billing.

Reports#

Payload: the report resource, identical to what GET /v1/reports/{report_id} returns: report_id, report_type, status, interval_start_at, interval_end_at, timezone, currency, created_at, and then either a download (with report_download_id, url, and expires_at) or a failure_reason.

EventFires when
report.succeededA report finished generating and its CSV is downloadable.
report.failedGeneration failed with generation_failed, data_unavailable, or limit_exceeded.

Use these instead of polling. The download in a report.succeeded payload expires 24 hours later, so download on receipt rather than storing the URL. See Reports.

Merchant Readiness#

Payload: the merchant's readiness snapshot: merchant_id, a payments and a payouts axis (each with status, status_reason, and machine-readable next_actions), the requirements lists (currently_due, past_due, eventually_due, pending_verification), and observed_at.

EventFires when
merchant.readiness.updatedAccount readiness was observed after a verification or account change, in live or sandbox mode.

Treat this event as an at-least-once fetch trigger, never as a change notification: deliveries can repeat, and an event can arrive with no visible change in the public state. On receipt, read GET /v1/onboarding/state (or GET /v1/merchants/{merchant_id}/readiness) and act on that authoritative snapshot. Sandbox processor transitions emit the same event, so you can rehearse the loop before going live. The webhooks guide covers the polling contract this event accelerates.

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.

text
buyer pays on the hosted page
  -> payment_intent.succeeded       the charge settled
  -> order.paid        the order is paid
  -> checkout_session.completed     the session finished; carries order_id
  -> balance_transaction.created    the charge hit your balance

Fulfill from order.paid or checkout_session.completed, never both.

Manual capture#

You place a hold now and capture later. See Manual Capture.

text
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                                    cancel, or let the hold expire
  -> payment_intent.succeeded              -> payment_intent.canceled
  -> order.payment_captured                -> order.payment_authorization_canceled
  -> order.paid                  or order.payment_authorization_expired

A partial capture fires order.payment_captured without order.paid.

Refund#

text
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.

Return#

A buyer returns merchandise and is refunded for it. Steps vary by flow: an in-store return records receipt, decision, and resolution in one call, so its events arrive together.

text
buyer requests a return
  -> return.created
merchant or policy automation decides
  -> return.decision_recorded
warehouse records what physically arrived
  -> return_receipt.created
  -> return_inspection.created
  -> return_inspection.acceptance_decided
  -> return_disposition.created -> return_disposition.updated
buyer value is proposed and confirmed
  -> return_resolution.created -> return_resolution.updated
  -> refund.created -> refund.updated    a refund resolution settles as a normal refund
  -> order.refunded
every merchandise and value obligation clears
  -> return.completed

When a job needs every obligation cleared, act on return.completed rather than on the resolution reaching a terminal state; a resolution can settle while merchandise work is still open. return.updated fires throughout as line-level progress changes, so treat it as a trigger to re-read the Return, not as a stage marker.

Dispute#

text
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#

text
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.paid        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#

text
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:

Bash
curl "https://api.withflintpay.com/v1/webhook-event-types?page_size=100" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": [
    {
      "event_type": "balance.updated",
      "event_sources": ["merchant"]
    },
    {
      "event_type": "balance_transaction.created",
      "event_sources": ["merchant"]
    }
  ],
  "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.

JSON
{
  "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"]
  }
}
EventFires when
partner_app.install.createdA merchant installed your app.
partner_app.install.updatedThe install changed state.
partner_app.install.permissions_updatedThe merchant changed your app's granted scopes.
partner_app.install.revokedThe merchant uninstalled your app.
partner_app.install.environment_grant.createdThe install was granted access to a merchant environment.
partner_app.install.environment_grant.revokedAn 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.paid, and checkout_session.completed all fire for one hosted payment. Pick one primary event per job and dedupe on webhook_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 as payment_intent.succeeded) and no refund.succeeded (success is refund.updated with status: "succeeded").
  • Treating the payload as current state. data is a snapshot from created_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 2xx for 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.
Rate this doc