Webhook event payloads

Flint sends webhook events for checkout payments, refunds, disputes, and subscription renewals. Each event type identifies what happened. Use its data payload to update your integration.

Two related pages cover the rest:

  • To receive events (register endpoints, verify signatures, handle retries), start with Webhooks.
  • For every event type with its payload schema and an example, see the live event catalog, generated from the server.
Note:

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",
  "request": null,
  "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" },
        "subtotal_money": { "amount": 9900, "currency": "USD" },
        "total_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.

requestobject | nullRequired

Always present. null when the event has no originating API request. Otherwise an object containing both the request id and idempotency_key.

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

Handle webhook events with these delivery guarantees in mind:

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

Warning: 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.
Keep subscription shipments movingsubscription.delivery_action_requiredA renewal of a plan that ships is held because delivery can't be quoted. Pair with subscription.delivery_pause_upcoming. Ship the renewals themselves from order.fulfillment.*.
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.

One flow, several events#

A single business flow emits events at several levels of the model. A successful payment on a subscription invoice can produce all of these:

  • payment_intent.succeeded
  • order.paid
  • invoice.paid
  • subscription.payment_succeeded
  • balance_transaction.created

That is intentional. Subscribe to the event closest to the job your system owns: payment intent and balance events for ledger work, order events for fulfillment, invoice events for receivables, subscription events for access, and payment method events for cards on file. Prefer subscription events over invoice or payment intent events when the action is about access to a recurring product.

A few pairs are easy to confuse:

  • invoice.sent reports the email. Use invoice.issued to know an invoice is collectible.
  • invoice.reminder_due is for sending the reminder yourself under caller_managed delivery. If Flint sends the email, use invoice.delivery_succeeded and invoice.delivery_failed.
  • invoice.credited is not final. credit_note.allocation_reversed can reopen the balance, so a ledger that treats credited as terminal will drift.
  • checkout_session.invalidated is not abandonment when it carries superseding_checkout_session_id: the buyer is using a newer session.
  • merchant.readiness.updated uses Flint's readiness vocabulary. Do not infer readiness from raw provider account flags.

When in doubt, use the highest-level event that matches the action your system takes. Fetch the resource before an irreversible side effect, and use resource_updated_at or a version field to ignore stale deliveries.

Payloads by family#

Subscribe per endpoint with enabled_events; an empty list means every event. Below is what each family's data carries and the behavior your handler should expect. For every event type with its full schema, use the live event catalog, which is generated from the server and always current. Families not covered here, such as delivery configuration, risk reviews, and fraud warnings, are listed there too.

Payment intents#

Payload for payment status events: data.payment_intent contains payment_intent_id, status, and amount_money, plus order_id, customer_id, and cancellation_reason when set. 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"
  }
}

payment_intent.fulfillment_hold.updated uses a different shape: data.payment_intent_id and data.fulfillment_hold are top-level fields, with data.order_id when the payment intent belongs to an order. It has no data.payment_intent wrapper.

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.

  • order.partially_paid
    The order records its first partial settlement.
    payment_statuspaid_moneyoutstanding_moneyorder_payment_intent_idsline_items
  • order.paid
    The order reaches full settlement and closes.
    payment_statuspaid_moneyoutstanding_moneyorder_payment_intent_idsline_items
  • order.payment_authorized
    A hold was placed and is waiting for capture.
    authorized_moneycapturable_moneyauthorization_expires_at
  • order.payment_captured
    Some or all of a held authorization was captured.
    captured_money
  • order.payment_authorization_canceled
    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_idrefund_amount_moneyrefunded_total_money

A partial capture fires order.payment_captured without order.paid. See Manual capture for the full authorize-then-capture flow. The family also has order.created, order.updated, and order.closed for mirroring order state; order.updated covers material edits, not payment changes, which have the dedicated events above. Every order event.

Order fulfillment#

Payload: order_id and fulfillment_id, plus summaries rather than full objects. order carries status and fulfillment_status. fulfillment carries type, status, request_status, resource_url, timestamps, and references, but not the recipient or line items. Shipment and package events add shipment and package summaries with their status updates. order.fulfillment.event.created carries only the fulfillment_event.

status and request_status match the fetched fulfillment: acceptance leaves status as pending and sets request_status to accepted, a hold preserves the status from before the hold, and a no-show reports failed. Fetch resource_url when you need the full resource before acting on a fulfillment event. See Fulfillment and shipping for the flow these events report.

order.fulfillment.status_changed carries previous_status and current_status. They record the step that happened, so they can be accepted, on_hold, or no_show, values the fulfillment's own status never shows. order.fulfillment.event.created fires for every recorded fact: a state change, a tracking change, a package status change, or a carrier or 3PL update you posted.

payment_intent.fulfillment_hold.updated fires when the payment provider asks to pause shipping for a payment, or lifts that request. When the pause starts, open fulfillments go on hold with reason payment_review.

Inventory#

Payload: embeds the applicable public inventory level, reservation, count, transfer, or receipt. 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.

inventory.level.updated can coalesce closely spaced changes into one event carrying the latest revision, so intermediate states are not delivered individually. inventory.transfer.closed can arrive together with the event for the transition's physical effect, such as inventory.transfer.received. Every inventory event.

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.

Reading inventory levels does not emit events. Reads do not change stock or create claims.

Two related events live on the order rather than on inventory: order.inventory_exception.created when an order's inventory failure needs operator action, and order.inventory_exception.resolved when it clears.

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"
}

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 (null when unset), failure_reason when the refund failed, per-payment splits in payment_refunds, and per-line-item detail in line_item_allocations. Older event retries and resends can lack reason; fetch the refund if it is missing. A refund raised by a Return resolution adds return_id and return_resolution_id, and one raised by a credit note adds credit_note_id and invoice_id. Refunds you create directly omit all four.

JSON
{
  "refund_id": "ref_1kmn0aExample",
  "status": "succeeded",
  "reason": "requested_by_customer",
  "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" }
    }
  ]
}

refund.created is sent when an API request, Return, or credit note creates a refund. Its payload has status: "pending". refund.updated reports acceptance and later status changes. There is no refund.succeeded event. Success is refund.updated with status: "succeeded", and refund.failed carries failure_reason. Events can repeat or arrive out of order, so fetch the refund and act on its current status. 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 events follow the Return model: return.* for the Return itself, including return.decision_recorded, return.completed, and return.reopened; return_receipt.*, return_inspection.*, and return_disposition.* for physical facts recorded against the merchandise; and return_resolution.* for buyer value.

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, status_changed_at, reason, and case_type, plus evidence state such as evidence_due_at, action_required, has_evidence, evidence_submission_count, and response_unavailable_reason when present.

Act on dispute.needs_response, which means your evidence is needed and a deadline is running. dispute.won and dispute.lost carry the outcome, and dispute.closed means no further action is required. dispute.prevented and dispute.warning_closed close early-warning cases that never became disputes. Every dispute event.

Subscriptions#

Payload: subscription_id, subscription_plan_id, and the subscription's line_items; each event adds its own context fields, listed below. A subscription created from a subscribe-and-save offer has no plan, so its events omit subscription_plan_id. subscription.updated, subscription.renewal_upcoming, subscription.dunning_exhausted, subscription.cancellation_scheduled, subscription.reactivated, subscription.cycle_skipped, subscription.delivery_updated, subscription.delivery_action_required, and subscription.delivery_pause_upcoming are compact: they carry subscription_id and their own fields only, with no subscription_plan_id or line_items.

JSON
{
  "subscription_id": "sub_1kmn0aExample",
  "subscription_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"
}
  • subscription.created
    The subscription was created.
    customer_id, status, optional next_billing_at
  • subscription.updated
    Schedule fields changed without a more specific lifecycle event, or upcoming_delivery_hold or inventory_wait was set or cleared. For those two, changed_fields lists the field that changed and previous_values holds its earlier value, null when it was just set. Flint sends initiated_by: "system" when it sets or clears them itself, and the actor, such as buyer or merchant, when a request clears them. Fetch the subscription to reconcile current state.
    changed_fieldsprevious_valuesinitiated_by
  • subscription.activated
    A trial ended and the subscription became active.
  • subscription.trial_ending
    A trial is approaching its end.
    trial_end
  • subscription.renewal_upcoming
    A scheduled renewal is approaching. delivery is the preference the renewal will ship with, in the same shape as the subscription's delivery.
    next_billing_at, delivery when the subscription ships
  • subscription.payment_succeeded
    A billing cycle charged successfully. delivery is the preference the renewal order was quoted and shipped with.
    order_id, payment_intent_id, delivery when the subscription ships
  • subscription.payment_failed
    A billing attempt failed. Retries follow.
    scheduled_billing_atorder_id
  • subscription.past_due
    Billing failed and the subscription is past due while retries run.
    status
  • subscription.dunning_exhausted
    Automatic payment retries were exhausted.
    dunning_end_action, failed-period timestamps, scheduled_billing_at, optional order and payment-attempt IDs
  • subscription.cancellation_scheduled
    The subscription was set to cancel at the end of its billing period, by the buyer or by you. subscription.canceled follows when it ends.
    cancel_at, cancellation_details without comment
  • subscription.reactivated
    A scheduled cancellation was undone, so the subscription renews as usual.
    initiated_by
  • subscription.canceled
    The subscription was canceled by request, at period end, or by its configured dunning action.
    reason when applicable
  • subscription.paused
    The subscription was paused by request, by its configured dunning action, or because a delivery hold ran out. A delivery pause sends a compact payload instead: subscription_id, customer_id, pause_reason: "delivery_action_required", reason (the hold's reason), and the delivery_hold that ran out, with reason, fixable_by, started_at, and ends_at. The subscription itself no longer reports delivery_hold once paused.
    reason when applicable
  • subscription.resumed
    A paused subscription resumed billing.
    reason
  • subscription.cycle_skipped
    The buyer or you skipped a cycle. next_billing_at is the new billing date.
    initiated_bynext_billing_at
  • subscription.delivery_updated
    The delivery preference of a subscription that ships changed. changed_by is buyer, merchant, or migration, and delivery is the new preference. open_renewal_order_id names a paid renewal order that hasn't shipped and is still going to the previous address; it is absent when there is none, and the signup order never counts.
    changed_by, delivery, optional open_renewal_order_id
  • subscription.delivery_action_required
    A renewal is held because delivery can't be quoted. Nothing was charged. Flint creates no renewal order during the hold, and cancels an unpaid renewal order already open for that billing date; a new one is created when the hold clears. reason is method_unavailable, destination_not_served, or rate_unavailable. delivery_hold carries reason, fixable_by (buyer or merchant), started_at, and ends_at.
    customer_idreasondelivery_hold
  • subscription.delivery_pause_upcoming
    A held subscription pauses in three days, or sooner on short intervals, unless delivery is fixed. delivery_hold.ends_at is when it pauses.
    customer_idreasondelivery_hold

subscription_delivery_migration.completed carries the migration instead of a subscription: subscription_delivery_migration_id, status, from_delivery_method_id, to_delivery_method_id, subscription_plan_id when the move was limited to one plan, total_count, moved_count, failed_count, pending_count, failure_reason_counts, and its timestamps. List the failed subscriptions from the migration's failures route.

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. On a plan that ships, the renewal order also gets a fulfillment and sends order.fulfillment.* events, and it carries subscription_cycle so you can spot first shipments. An Order now renewal sends the same subscription.payment_succeeded and order events as a scheduled one. See Subscription billing and Ship physical products.

Payload for payment retry events: a payment retry is a charge of a past-due subscription that the buyer starts from their account, or you start through the API. Its events carry the retry as data.object: subscription_payment_retry_id, subscription_id, idempotency_key, status (pending, processing, succeeded, or failed), created_at, and updated_at, plus started_at, completed_at, order_id, order_payment_attempt_id, and a buyer-safe failure with code and message once they apply.

JSON
{
  "object": {
    "subscription_payment_retry_id": "spr_1kmn0aExample",
    "subscription_id": "sub_1kmn0aExample",
    "idempotency_key": "recover-subscription-1",
    "status": "succeeded",
    "order_id": "ord_1kmn0aExample",
    "order_payment_attempt_id": "opat_1kmn0aExample",
    "created_at": "2026-10-05T14:00:00Z",
    "updated_at": "2026-10-05T14:01:00Z",
    "started_at": "2026-10-05T14:00:02Z",
    "completed_at": "2026-10-05T14:01:00Z"
  }
}

Use the retry events to report how a retry went, for example in your own dunning email, and keep granting and revoking access from the subscription events above. See Recover a past-due subscription.

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.

invoice.issued means the invoice number and billing snapshot were assigned and the invoice is collectible. invoice.payment_failed leaves the invoice collectible. invoice.reminder_due carries delivery_mode, which says whether Flint sent the reminder or left it to you. invoice.credited fires when credit note allocations close the invoice, and invoice.marked_uncollectible when you write the balance off. Every invoice event.

invoice.reminder_due, invoice.overdue, and invoice.late_fee_due are scheduled from the due date at issue and only fire while a balance remains. They add due_at, and invoice.late_fee_due adds late_fee_money, base_outstanding_money, and late_fee_policy. invoice.late_fee_assessed and invoice.late_fee_waived follow your call to the late fee endpoints rather than a schedule, and both add invoice_late_fee_id, amount_money, and outstanding_money.

See Invoicing.

Credit notes#

Payload: credit_note.* events carry the credit note: credit_note_id, invoice_id, credit_note_number once issued, reason, status, credit_note_lines, total_money, and unallocated_money. The two allocation events carry the allocation instead: credit_note_allocation_id, credit_note_id, invoice_id, amount_money, allocated_at, idempotency_key, and reversed_at on a reversal.

credit_note.issued assigns the credit note number and freezes the lines. credit_note.allocation_created applies credit to the invoice, and credit_note.allocation_reversed reopens that balance.

Issuing a credit note does not change the invoice; the allocation does. Pair credit_note.allocation_created with invoice.credited when you need to know the invoice closed. See Credit notes.

Customers and saved payment methods#

Payload: customer.created carries customer_id, email, and name, whether you created the customer or Flint created it from a buyer's email at checkout; 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.

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. See Save a card and charge it later.

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.

customer.deletion_requested arrives with status: "pending_review". customer.deletion_completed means the request was approved and the customer was anonymized, and customer.deletion_rejected means it 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 deletion requests.

Payouts#

Payload: the payout: payout_id, amount_money, status, method, initiated_by, fee fields, and payout_destination_id when set. For the payout lifecycle and how to handle a failure, see Payouts.

payout.created means scheduled, not arrived. payout.paid means the money reached your bank. payout.failed returns the funds to your balance, and payout.reversed reports a completed payout that was pulled back. Every payout event.

Payout destinations and settings#

Payload: the payout destination or settings object that changed, with public status values. Every payout destination event and payout_settings.updated.

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 typed related_resource reference back to its source when available. occurred_at records when the transaction occurred. 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: positive for charges and negative for returned fees at API version 2026-09-07. On a payment it matches the payment's processing_fee_money (Processing fees); on movement types that carry no Flint fee, including refunds, disputes, standard payouts, and Flint billing collections, it is zero. capability.updated carries the capability name, its status, and outstanding requirements.

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.

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.

Use report.succeeded and report.failed instead of polling. failure_reason is generation_failed, data_unavailable, or limit_exceeded. 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.

merchant.readiness.updated fires after a verification or account change, in live or sandbox mode. Treat it 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/merchant 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.

Custom domains#

Payload: data.object is the hostname's status after the change. domain_type says which hostname it is, checkout or customer_account, and previous_status holds the status before the change. The rest matches the custom domain status in GET /v1/settings: hostname, domain_status, status_reason, dns_records, last_checked_at, redirect_expires_at, payment_method_domain_id, and, for a checkout hostname, active_payment_attempt_count.

JSON
{
  "object": {
    "domain_type": "checkout",
    "previous_status": "provisioning",
    "hostname": "pay.example.com",
    "domain_status": "active",
    "dns_records": [
      { "dns_record_type": "cname", "name": "pay.example.com", "value": "checkout-custom.withflintpay.com" },
      { "dns_record_type": "txt", "name": "_flint-verify.pay.example.com", "value": "flint-verify=..." }
    ],
    "last_checked_at": "2026-10-05T14:00:00Z",
    "payment_method_domain_id": "pmdom_1kmn0aExample",
    "active_payment_attempt_count": 0
  }
}

The event fires each time domain_status changes, including when the hostname is removed. A new last_checked_at alone doesn't send it. When the status isn't active, status_reason says what to fix, such as dns_record_missing or caa_record_blocks_certificate, and on removed the dns_records are the records you can delete. Both domain_status and status_reason can gain values, so treat one you don't recognize as needing attention. See Custom domains.

Event sequences by scenario#

Note:

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.

  1. What happens: The buyer pays on the hosted page
  2. Flint sends: payment_intent.succeeded
    The charge settled.
  3. Flint sends: order.paid
    The order is paid.
  4. Flint sends: checkout_session.completed
    The session finished. Carries order_id.
  5. Flint sends: 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.

  1. What happens: You confirm with manual capture
  2. Flint sends: payment_intent.requires_action
    Only if 3D Secure is required.
  3. Flint sends: payment_intent.requires_capture
    The hold is on.
  4. Flint sends: order.payment_authorized
    Carries capturable_money.
  5. What happens: You capture
  6. Flint sends: payment_intent.succeeded
    The captured amount settled.
  7. Flint sends: order.payment_captured
    The order records the capture.
  8. Flint sends: order.paid
    Only when the capture covers the order.
  9. What happens: You cancel, or let the hold expire
  10. Flint sends: payment_intent.canceled
    The hold is released.
  11. Flint sends: order.payment_authorization_canceledororder.payment_authorization_expired
    Canceled by you, or expired after the authorization window.

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

Refund#

  1. What happens: You call POST /v1/refunds
  2. Flint sends: refund.created
    Created. Status pending.
  3. Flint sends: refund.updated
    Accepted. Status pending, or already succeeded.
  4. Flint sends: order.refunded
    Order totals updated.
  5. What happens: The refund settles
  6. Flint sends: refund.updated
    Status succeeded.
  7. Flint sends: balance_transaction.created
    The debit on your balance.
  8. What happens: If the refund fails
  9. Flint sends: refund.updated
    Status failed.
  10. Flint sends: 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.

  1. What happens: The buyer requests a return
  2. Flint sends: return.created
    A Return was created.
  3. What happens: You or policy automation decide
  4. Flint sends: return.decision_recorded
    The merchant or policy automation recorded line-level Return decisions.
  5. What happens: The warehouse records what physically arrived
  6. Flint sends: return_receipt.created
    A Return receipt observation was recorded.
  7. Flint sends: return_inspection.created
    A Return inspection observation was recorded.
  8. Flint sends: return_inspection.acceptance_decided
    Acceptance was decided for a Return inspection observation.
  9. Flint sends: return_disposition.created
    A merchandise disposition was recorded for a Return.
  10. Flint sends: return_disposition.updated
    A Return disposition changed execution state.
  11. What happens: Buyer value is proposed and confirmed
  12. Flint sends: return_resolution.created
    A proposed buyer-value resolution was created for a Return.
  13. Flint sends: return_resolution.updated
    A Return resolution changed proposal, gate, effect, or fulfillment state.
  14. Flint sends: refund.created
    A refund resolution settles as a normal refund.
  15. Flint sends: refund.updated
    A refund changed state.
  16. Flint sends: order.refunded
    An order received a full or partial refund.
  17. What happens: Every merchandise and value obligation clears
  18. Flint sends: return.completed
    A Return completed after every required merchandise and buyer-value obligation cleared.

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#

  1. What happens: The card network opens a case
  2. Flint sends: dispute.created
    A payment dispute was opened.
  3. Flint sends: dispute.needs_response
    The evidence deadline is running.
  4. What happens: You submit evidence
  5. Flint sends: dispute.updated
    The case is under review.
  6. Flint sends: dispute.wonordispute.lost
    The network decided.
  7. Flint sends: dispute.closed
    A dispute was closed without further merchant action.
  8. What happens: An early-warning case
  9. Flint sends: dispute.preventedordispute.warning_closed
    The warning ended without a chargeback, or was closed.

Subscription billing#

  1. What happens: Signup completes
  2. Flint sends: subscription.created
    Hosted signup also sends customer.created and payment_method.saved.
  3. What happens: Each successful cycle
  4. Flint sends: subscription.payment_succeeded
    Carries the cycle's order_id.
  5. Flint sends: order.paid
    The cycle's order, like any other order.
  6. What happens: The trial ends
  7. Flint sends: subscription.activated
    A subscription became active.
  8. What happens: A renewal fails
  9. Flint sends: subscription.payment_failed
    A subscription billing attempt failed.
  10. Flint sends: subscription.past_due
    Retries are running.
  11. What happens: Retries recover
  12. Flint sends: subscription.payment_succeeded
    A subscription billing attempt succeeded.
  13. What happens: Retries exhaust
  14. Flint sends: subscription.dunning_exhausted
    Outcome is cancel, pause, or notify_only.
  15. What happens: The buyer or you cancel at the end of the period
  16. Flint sends: subscription.cancellation_scheduled
    Carries cancel_at and who asked.
  17. Flint sends: subscription.reactivated
    Only if the cancellation is undone before then.
  18. Flint sends: subscription.canceled
    When the period ends, with reason: "period_end".

The entitlement pattern: provision on subscription.payment_succeeded, warn on subscription.past_due, revoke on subscription.canceled.

Payout cycle#

  1. What happens: Funds are scheduled to your bank
  2. Flint sends: payout.created
    A payout to the merchant's bank was created.
  3. Flint sends: payout.updated
    A payout changed state.
  4. Flint sends: payout.paidorpayout.failed
    Paid, or failed with the funds returned to your balance.
  5. What happens: After arrival
  6. Flint sends: 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
curl "https://api.withflintpay.com/v1/webhook-event-types?page_size=100" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "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.

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

Was this helpful?