Payloads
Field-level schema for Flo webhook payloads.
Payloads
Each Flo webhook message contains one normalized Flo event payload.
For concrete examples of delivered events, see Events.
Common Fields
{
"eventId": "9f6f8b54-8e2d-4f15-8c8a-d7b6d9f41a00",
"eventVersion": "v1",
"objectType": "subscription",
"eventType": "subscription.created",
"user": {
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "user_123",
"email": "user@example.com"
},
"subscription": {
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"id": "plan_pro",
"name": "Pro",
"description": "Pro subscription",
"startDate": "2026-04-10T00:00:00.000Z",
"endDate": "2026-05-10T00:00:00.000Z",
"state": "active",
"quantity": 1,
"currency": "USD",
"amount": 29,
"total": 29,
"cancelAt": null,
"canceledAt": null,
"trialEnd": null,
"pausedAt": null,
"resumeAt": null,
"metadata": null,
"invoiceDetails": null
}
}Event Envelope
eventId: The Flo webhook delivery identifier for this endpoint delivery. It is not the upstream gateway event id, and automatic retries for the same delivery reuse the sameeventId.eventVersion: The webhook schema version.objectType: The top-level Flo resource family for the event:subscription,item,invoice,payment,dispute, orpre_dispute_alert.eventType: The normalized Flo event type.user: The canonical user object for the event. On dispute and Pre-Dispute Alert events it carries onlyidandclientUserId, and it is omitted when Flo cannot resolve exactly one user.subscription: Present on subscription events. May also be present on invoice events as related enrichment.item: Present on item events. May also be present on invoice events as related enrichment.invoice: Present on invoice events.payment: Present on payment authorization lifecycle events. May also be present onitem.purchasedas the Flo payment behind the purchase; see Item Purchase Payment.dispute: Present on dispute lifecycle events (objectType: "dispute").preDisputeAlert: Present on Pre-Dispute Alert events (objectType: "pre_dispute_alert").
Checkout metadata
The merged webhook DTOs expose an optional checkout snapshot on the event's primary object:
For create-time use and the record-by-record copy lifecycle, see preserve checkout context with metadata.
| Event DTO | Primary object | Field |
|---|---|---|
WebhookPaymentEventDto | payment | payment.checkoutMetadata |
WebhookItemEventDto | item | item.checkoutMetadata |
WebhookSubscriptionEventDto | subscription | subscription.checkoutMetadata |
WebhookInvoiceEventDto | invoice | invoice.checkoutMetadata |
The field type is Record<string, string> when present. When the source snapshot is absent, including an omitted or null create value, the webhook key is omitted. An explicit {} snapshot is preserved as {}.
checkoutMetadata is never an event-root field. On invoice events, it appears only on invoice; related root-level subscription and item enrichment keep their existing metadata fields and do not duplicate the checkout snapshot. On item.purchased, the optional root-level payment object can carry the payment's own snapshot as payment.checkoutMetadata, just as it does on payment events.
Dispute and Pre-Dispute Alert events carry no checkoutMetadata. Correlate them through paymentIds, which reference the same Flo payment ids that payment events expose as payment.id.
Payment primary object
{
"eventId": "9f6f8b54-8e2d-4f15-8c8a-d7b6d9f41a01",
"eventVersion": "v1",
"objectType": "payment",
"eventType": "payment.authorized",
"user": {
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "buyer-123",
"email": "buyer@example.com"
},
"payment": {
"id": "3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0",
"checkoutSessionId": "fd4475e4-dc19-4439-b830-c9c8f67a35e7",
"amount": 49.99,
"currency": "USD",
"status": "authorized",
"authorizationExpiresAt": "2026-08-26T14:30:00.000Z",
"authorizationVoidReason": null,
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}
}Item primary object
{
"eventId": "9f6f8b54-8e2d-4f15-8c8a-d7b6d9f41a02",
"eventVersion": "v1",
"objectType": "item",
"eventType": "item.purchased",
"user": {
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "buyer-123",
"email": "buyer@example.com"
},
"item": {
"uuid": "da634f98-8748-4e90-bb5d-f3df74003b57",
"id": "starter",
"name": "Starter",
"description": "Starter access",
"quantity": 1,
"currency": "USD",
"amount": 49.99,
"total": 49.99,
"invoiceState": "paid",
"metadata": {
"catalogReference": "starter"
},
"checkoutMetadata": {
"merchantOrderId": "order-123"
},
"invoiceDetails": null
},
"payment": {
"id": "4e676a49-976c-4f37-8e15-2b7a64f72891",
"checkoutSessionId": "0d8f2f9e-6a0e-4c43-9a51-3f3c3b8c7d10",
"amount": 79.97,
"currency": "USD",
"status": "succeeded",
"authorizationExpiresAt": null,
"authorizationVoidReason": null,
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}
}Subscription primary object
{
"eventId": "9f6f8b54-8e2d-4f15-8c8a-d7b6d9f41a03",
"eventVersion": "v1",
"objectType": "subscription",
"eventType": "subscription.created",
"user": {
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "buyer-123",
"email": "buyer@example.com"
},
"subscription": {
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"id": "plan_starter",
"name": "Starter",
"description": "Starter subscription",
"startDate": "2026-08-25T00:00:00.000Z",
"endDate": "2026-09-25T00:00:00.000Z",
"state": "active",
"quantity": 1,
"currency": "USD",
"amount": 49.99,
"total": 49.99,
"cancelAt": null,
"canceledAt": null,
"trialEnd": null,
"pausedAt": null,
"resumeAt": null,
"metadata": {
"catalogReference": "plan_starter"
},
"checkoutMetadata": {
"merchantOrderId": "order-123"
},
"invoiceDetails": null
}
}Invoice primary object
{
"eventId": "9f6f8b54-8e2d-4f15-8c8a-d7b6d9f41a04",
"eventVersion": "v1",
"objectType": "invoice",
"eventType": "invoice.paid",
"user": {
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "buyer-123",
"email": "buyer@example.com"
},
"invoice": {
"id": "7bb5ce14-59e0-41ee-8fb7-84d6967cdb68",
"invoiceNumber": "FLO-1001",
"state": "paid",
"billingReason": "subscription_cycle",
"currency": "USD",
"amountDue": 49.99,
"amountPaid": 49.99,
"subtotal": 49.99,
"total": 49.99,
"paidAt": "2026-08-25T10:20:00.000Z",
"hostedInvoiceUrl": "https://merchant.example/invoice",
"invoicePdfUrl": "https://merchant.example/invoice.pdf",
"invoiceDetails": null,
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
},
"subscription": {
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"id": "plan_starter",
"name": "Starter",
"description": "Starter subscription",
"quantity": 1,
"currency": "USD",
"amount": 49.99,
"total": 49.99,
"metadata": {
"catalogReference": "plan_starter"
}
},
"item": {
"uuid": "da634f98-8748-4e90-bb5d-f3df74003b57",
"id": "starter",
"name": "Starter",
"description": "Starter access",
"quantity": 1,
"currency": "USD",
"amount": 49.99,
"total": 49.99,
"metadata": {
"catalogReference": "starter"
}
}
}Correlate after deduplication
Use the top-level eventId to deduplicate webhook delivery. Payment and invoice events use the primary entity id as the FloPay record identifier. Item and subscription events also expose a provider catalog id, so use their primary entity uuid as the FloPay record identifier. Use checkoutMetadata.merchantOrderId only for business correlation after the event has been reserved successfully.
type CheckoutMetadata = Record<string, string>;
type IdPrimary = { id: string; checkoutMetadata?: CheckoutMetadata };
type UuidPrimary = { uuid: string; checkoutMetadata?: CheckoutMetadata };
type FloEvent =
| { eventId: string; objectType: 'payment'; payment: IdPrimary }
| { eventId: string; objectType: 'item'; item: UuidPrimary }
| { eventId: string; objectType: 'subscription'; subscription: UuidPrimary }
| { eventId: string; objectType: 'invoice'; invoice: IdPrimary };
declare function reserveEvent(eventId: string): Promise<boolean>;
declare function correlateOrder(
merchantOrderId: string,
objectType: FloEvent['objectType'],
entityId: string,
): Promise<void>;
export async function handleFloEvent(event: FloEvent) {
if (!(await reserveEvent(event.eventId))) return;
const { primary, entityId } =
event.objectType === 'payment'
? { primary: event.payment, entityId: event.payment.id }
: event.objectType === 'item'
? { primary: event.item, entityId: event.item.uuid }
: event.objectType === 'subscription'
? { primary: event.subscription, entityId: event.subscription.uuid }
: { primary: event.invoice, entityId: event.invoice.id };
const merchantOrderId = primary.checkoutMetadata?.merchantOrderId;
if (!merchantOrderId) return;
await correlateOrder(merchantOrderId, event.objectType, entityId);
}Compatibility Fields
New integrations should build against user, eventType, objectType, and the explicit resource root (subscription, item, invoice, payment, dispute, or preDisputeAlert).
On invoice events, root-level subscription and item are enrichment fields. Flo includes them when the invoice points at a linked local transaction that resolves to a checkout subscription and/or checkout item.
User Object
{
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "user_123",
"email": "user@example.com"
}Dispute and Pre-Dispute Alert events use a reduced user object with no email, and omit it entirely when Flo cannot resolve exactly one user:
{
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "user_123"
}Payment Payload
Payment authorisation events include the provider-neutral payment state. id is FloPay's payment UUID for the trusted-server capture and cancellation operations; it is not a provider intent id.
{
"id": "3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0",
"checkoutSessionId": "fd4475e4-dc19-4439-b830-c9c8f67a35e7",
"amount": 49.99,
"currency": "GBP",
"status": "authorized",
"authorizationExpiresAt": "2026-08-11T14:30:00.000Z",
"authorizationVoidReason": null
}checkoutSessionId: FloPay checkout session associated with the payment.amount/currency: Full authorised amount in the payment currency.status: Canonical payment state. Authorisation events useauthorizedorvoided; see the payment state reference.authorizationExpiresAt: Provider-derived capture deadline for an active hold.authorizationVoidReason:merchant_requested,expired, orprovider_canceledafter a hold becomesvoided; otherwisenull.
item.purchased events can carry the same object as the Flo payment behind a purchase. See Item Purchase Payment for its purchase-specific meaning.
Dispute Payload
Dispute events include the provider-neutral dispute state. id is Flo's dispute UUID and stays the same for every event about that dispute; it is not a provider dispute id. See Dispute events for delivered examples and the lifecycle rules.
{
"id": "6f1c2a0e-5b7d-4e3a-9c48-2d1f0b7e9a11",
"status": "under_review",
"phase": "initial",
"actionRequired": false,
"responseDueAt": null,
"amount": 49.99,
"currency": "USD",
"reason": "fraud_or_unauthorized",
"paymentIds": ["3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0"],
"linkStatus": "linked",
"sequence": 841832101,
"createdAt": "2026-09-04T10:15:00.000Z",
"updatedAt": "2026-09-05T08:40:00.000Z"
}| Field | Type | Meaning |
|---|---|---|
id | string | Flo dispute UUID. Stable across every event for the dispute. |
status | enum | open, under_review, won, or lost. won and lost are terminal; see lifecycle and terminal outcomes. |
phase | enum | initial for the first round, appeal for a further round after a decision. |
actionRequired | boolean | true while the provider is waiting for your evidence or refund. |
responseDueAt | string, nullable | ISO 8601 deadline for the required action. Set only while actionRequired is true, otherwise null. |
amount | number | Disputed amount in major units of currency. |
currency | string | ISO 4217 currency code in upper case. |
reason | enum | Normalized dispute reason. See dispute reasons. |
paymentIds | string[] | Flo payment UUIDs linked to the dispute. Zero, one, or several; empty while unlinked. |
linkStatus | enum | linked, partially_linked, unmatched, or action_required. See dispute link status. |
sequence | integer | Increases per id. It does not start at one and is only comparable within a single id. Apply a payload only when it exceeds the last sequence you stored for that id. |
createdAt | string | When Flo first recorded the dispute. |
updatedAt | string | When the reported state was observed. |
Dispute reasons
reason uses the same values on disputes and Pre-Dispute Alerts:
reason | Meaning |
|---|---|
fraud_or_unauthorized | The cardholder did not authorize or does not recognize the charge. |
duplicate | The cardholder was charged more than once for the same purchase. |
not_received | Goods or services were not received. |
not_as_described | Goods or services were defective or not as described. |
credit_not_processed | An expected refund or credit was not issued. |
cancelled_recurring | A recurring charge continued after cancellation. |
incorrect_amount | The charged amount was wrong. |
paid_by_other_means | The purchase was paid for another way. |
processing_error | A processing or bank problem, such as details that could not be processed. |
other | Any reason Flo cannot map to the values above. |
Dispute link status
linkStatus | Meaning |
|---|---|
linked | Every provider reference on the dispute resolved to a Flo payment. paymentIds is complete. |
partially_linked | Some references resolved. paymentIds holds the linked payments and the rest are queued for review. |
action_required | Flo could not link the dispute automatically and it is queued for review. paymentIds is empty. |
unmatched | A reviewer confirmed there is no matching Flo payment. paymentIds is empty. |
A link that is verified later keeps the same id, raises sequence, and arrives as dispute.updated.
Pre-Dispute Alert Payload
Pre-Dispute Alert events include the provider-neutral alert state. id is Flo's alert UUID and stays the same for the whole life of the alert, before and after it is linked to a payment. The payload never carries the alert source, its identifiers, its raw statuses, or matching evidence. See Pre-Dispute Alert events for delivered examples.
{
"id": "b2e7c9d4-1f3a-4c6e-8a5b-7d9e0f1a2b3c",
"status": "needs_response",
"actionRequired": true,
"reason": "fraud_or_unauthorized",
"responseDueAt": "2026-09-06T09:58:12.000Z",
"amount": 49.99,
"currency": "USD",
"alertAt": "2026-09-04T09:58:12.000Z",
"cardLast4": "4242",
"merchantDescriptor": "EXAMPLE*STORE",
"acquirerReferenceNumber": "24692166247000000000123",
"authorizationCode": "A1B2C3",
"paymentIds": [],
"linkStatus": "unmatched",
"sequence": 841831092,
"createdAt": "2026-09-04T09:58:12.000Z",
"updatedAt": "2026-09-04T09:58:12.000Z"
}| Field | Type | Meaning |
|---|---|---|
id | string | Flo alert UUID. Stable for the whole alert lifecycle. |
status | enum | needs_response, under_review, closed, prevented, or unknown. closed and prevented are terminal. |
actionRequired | boolean | true when you should act. An unknown status fails safe to true. |
reason | enum, nullable | fraud_or_unauthorized, duplicate, not_received, not_as_described, credit_not_processed, cancelled_recurring, incorrect_amount, paid_by_other_means, processing_error, or other, with the meanings in dispute reasons. null when the source gave no reason. |
responseDueAt | string, nullable | ISO 8601 deadline for a response. null when no action is required or the source gave no deadline. |
amount | number, nullable | Alerted amount in major units of currency. |
currency | string, nullable | ISO 4217 currency code in upper case. |
alertAt | string, nullable | When the alert source raised the alert. |
cardLast4 | string, nullable | Last four digits of the card. |
merchantDescriptor | string, nullable | The billing descriptor the cardholder saw. |
acquirerReferenceNumber | string, nullable | The acquirer reference number of the charge, when the source provided it. |
authorizationCode | string, nullable | The authorization code of the charge, when the source provided it. |
paymentIds | string[] | Flo payment UUIDs linked to the alert. Empty while unlinked. |
linkStatus | enum | linked, unmatched, or action_required. See Pre-Dispute Alert link status. |
sequence | integer | Increases per id. It does not start at one and is only comparable within a single id. Discard deliveries that do not advance it. |
createdAt | string | When the alert was first observed. |
updatedAt | string | When the reported state was observed. |
Every nullable field can be absent at the source. A later notification that omits a field never clears the value an earlier notification established, so a pre_dispute_alert.updated carries the best-known state of the alert.
Pre-Dispute Alert link status
linkStatus | Meaning |
|---|---|
linked | The alert's references resolved to Flo payments. paymentIds is authoritative. |
unmatched | The alert carried references, but none matched a Flo payment. paymentIds is empty. |
action_required | The alert carried no reference Flo could match on. paymentIds is empty; correlate locally with the descriptive fields. |
Alerts have no partial link state. A link that is verified later keeps the same id, raises sequence, and arrives as pre_dispute_alert.updated with linkStatus: "linked".
Correlate an unlinked Pre-Dispute Alert
An alert can reach you before Flo has linked it to a customer or payment, and the link, if it ever arrives, comes later as pre_dispute_alert.updated for the same id. Keep exactly one local record per preDisputeAlert.id and let each delivery upgrade it, so the later link never creates a second record.
Deduplicate by eventId first. Then apply the payload only when its sequence is higher than the one you stored, because deliveries can arrive out of order. While linkStatus is unmatched or action_required, use the descriptive fields as best-effort hints for a local lookup: cardLast4, merchantDescriptor, acquirerReferenceNumber, authorizationCode, amount, currency, and alertAt. Any of them can be null, and a hint match is a suggestion for your team, not a verified link. Once linkStatus becomes linked, paymentIds is authoritative and replaces the hint-based guess.
type PreDisputeAlert = {
id: string;
status: 'needs_response' | 'under_review' | 'closed' | 'prevented' | 'unknown';
actionRequired: boolean;
reason:
| 'fraud_or_unauthorized'
| 'duplicate'
| 'not_received'
| 'not_as_described'
| 'credit_not_processed'
| 'cancelled_recurring'
| 'incorrect_amount'
| 'paid_by_other_means'
| 'processing_error'
| 'other'
| null;
responseDueAt: string | null;
amount: number | null;
currency: string | null;
alertAt: string | null;
cardLast4: string | null;
merchantDescriptor: string | null;
acquirerReferenceNumber: string | null;
authorizationCode: string | null;
paymentIds: string[];
linkStatus: 'linked' | 'unmatched' | 'action_required';
sequence: number;
};
type PreDisputeAlertEvent = {
eventId: string;
eventType: 'pre_dispute_alert.created' | 'pre_dispute_alert.updated';
user?: { id: string; clientUserId: string | null };
preDisputeAlert: PreDisputeAlert;
};
type AlertRecord = { sequence: number; orderIds: string[]; verified: boolean };
type AlertHints = Pick<
PreDisputeAlert,
| 'cardLast4'
| 'merchantDescriptor'
| 'acquirerReferenceNumber'
| 'authorizationCode'
| 'amount'
| 'currency'
| 'alertAt'
>;
declare function reserveEvent(eventId: string): Promise<boolean>;
declare function loadAlert(alertId: string): Promise<AlertRecord | null>;
declare function saveAlert(alertId: string, record: AlertRecord, alert: PreDisputeAlert): Promise<void>;
declare function findOrdersByPaymentIds(paymentIds: string[]): Promise<string[]>;
declare function findOrdersByHints(hints: AlertHints): Promise<string[]>;
export async function handlePreDisputeAlert(event: PreDisputeAlertEvent) {
if (!(await reserveEvent(event.eventId))) return;
const alert = event.preDisputeAlert;
const existing = await loadAlert(alert.id);
if (existing && existing.sequence >= alert.sequence) return;
const verified = alert.linkStatus === 'linked';
// A stored empty array means the earlier hint lookup matched nothing, so retry it: a later
// delivery can carry a hint the first one omitted.
let orderIds = existing?.orderIds ?? [];
if (verified) {
orderIds = await findOrdersByPaymentIds(alert.paymentIds);
} else if (orderIds.length === 0) {
orderIds = await findOrdersByHints({
cardLast4: alert.cardLast4,
merchantDescriptor: alert.merchantDescriptor,
acquirerReferenceNumber: alert.acquirerReferenceNumber,
authorizationCode: alert.authorizationCode,
amount: alert.amount,
currency: alert.currency,
alertAt: alert.alertAt,
});
}
await saveAlert(alert.id, { sequence: alert.sequence, orderIds, verified }, alert);
}The record carries verified so that downstream automation, such as an automatic refund, runs only on a verified link or on a hint match a person has confirmed. user, when present, resolves to exactly one Flo user and is safe to use for customer lookup; when it is absent, the alert is still yours to act on.
Subscription Payload
uuid: Flo's checkout subscription UUID when the event is linked to a checkout session.id: The checkout subscription's provider plan id.name/description: The checkout subscription's provider-backed display fields.
{
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"id": "plan_pro",
"name": "Pro",
"description": "Pro subscription",
"startDate": "2026-04-10T00:00:00.000Z",
"endDate": "2026-05-10T00:00:00.000Z",
"state": "active",
"quantity": 1,
"currency": "USD",
"amount": 29,
"total": 29,
"cancelAt": null,
"canceledAt": null,
"trialEnd": null,
"pausedAt": null,
"resumeAt": null,
"metadata": null,
"invoiceDetails": null
}Automated subscription recovery does not add campaign, round, or recovered fields to webhook payloads. Use the existing event sequence to follow lifecycle state, then use subscription reporting for recovered attribution. During a campaign, subscription.state moves through past_due, active after confirmed recovery, or expired after terminal exhaustion. The paid-through dates in startDate and endDate remain unchanged on failed attempts.
Item Payload
uuid: Flo's checkout item UUID when the event is linked to a checkout session.id: The checkout item's provider item id.name/description: The checkout item's provider-backed display fields.amount/currency: The value and currency of the item event. Onitem.charged_back, the amount is money withdrawn by the provider dispute.
{
"uuid": "da634f98-8748-4e90-bb5d-f3df74003b57",
"id": "item_123",
"name": "Product",
"description": "Product description",
"quantity": 1,
"currency": "USD",
"amount": 29,
"total": 29,
"invoiceState": "paid",
"metadata": null,
"invoiceDetails": null
}For item money-movement events, use eventType as the primary branch and invoiceDetails.transactionType as the financial discriminator:
| Event | invoiceDetails.transactionType | Meaning |
|---|---|---|
item.refunded | refund | A genuine merchant or administrative refund. |
item.charged_back | chargeback | Money was withdrawn because of a provider dispute. |
The chargeback case keeps the existing item and invoiceDetails field shapes. For example:
{
"eventType": "item.charged_back",
"item": {
"amount": 29,
"currency": "USD",
"invoiceState": "voided",
"invoiceDetails": {
"transactionType": "chargeback",
"transactionId": "dp_123",
"chargeId": "ch_123",
"currency": "USD"
}
}
}A genuine refund carries invoiceDetails.transactionType: "refund". Do not infer a chargeback from item.refunded or from provider-specific trace fields.
Item Purchase Payment
item.purchased events carry an optional payment object at the event root, beside item. It identifies the Flo payment behind the purchase, so you can match fulfillment to a payment straight from the purchase event. It is the same object that payment.* events carry, with the same field names, enum values, and nullability: WebhookItemEventDto declares it as an optional WebhookPaymentDto. See the item primary object for a complete event.
{
"id": "4e676a49-976c-4f37-8e15-2b7a64f72891",
"checkoutSessionId": "0d8f2f9e-6a0e-4c43-9a51-3f3c3b8c7d10",
"amount": 79.97,
"currency": "USD",
"status": "succeeded",
"authorizationExpiresAt": null,
"authorizationVoidReason": null,
"checkoutMetadata": {
"merchantOrderId": "order-123"
}
}| Field | Type | Meaning |
|---|---|---|
id | string | Flo payment UUID, the ID of the Flo transaction behind the purchase. Never a provider identifier. |
checkoutSessionId | string, nullable | The checkout session the payment came from, or null for a payment without one, such as a Flo-managed renewal. |
amount | number | Full payment amount in major units of currency, not the item's share. |
currency | string | Currency of the full payment. |
status | enum | Payment state recorded when Flo created the event: pending, authorized, processing, succeeded, failed, refunded, partially_refunded, refund_failed, voided, or unknown. An event created late, such as after a delayed provider notification, can already show a later state like refunded or voided. See payment states. |
authorizationExpiresAt | string, nullable | ISO 8601 capture deadline of a manual-capture authorization hold. null when it does not apply. |
authorizationVoidReason | enum, nullable | merchant_requested, expired, or provider_canceled when an authorization hold was voided, otherwise null. |
checkoutMetadata | object, optional | The Record<string, string> checkout snapshot copied to the payment. Present only when the payment has one, and never null. |
checkoutMetadata is optional and omitted when the payment has no snapshot; every other field is present on every payment object, and a nullable field holds null when it does not apply.
Among item events, only item.purchased carries payment; item.refunded and item.charged_back are unchanged. The field is additive: eventVersion stays v1, existing item fields keep their meaning, and consumers that do not read payment need no change.
Reconcile against the full payment
payment.id is the Flo payment UUID, the ID of the Flo transaction behind the purchase. It is the same ID that payment.* events, dispute paymentIds, and the payments API use, and it is never a Stripe, PayPal, or other provider identifier. payment exposes nothing else from the transaction: no provider identifiers, provider statuses, fees, or internal metadata.
payment.amount and payment.currency describe the whole payment, not the item. item.amount and item.total keep describing the item's own line or allocation, so do not expect the two to match. When one payment covers several items, or items and subscriptions together, every item.purchased from that payment carries the same payment. Group those events by payment.id, and do not add payment.amount up across them.
When payment is absent
payment is optional. Flo omits it, and never sends it as null, in two cases:
- A zero-value purchase with no Flo payment. When a purchase takes no money, for example an order fully covered by a discount, Flo records no payment, so there is none to reference.
- An event created before
paymentwas added. Events queued or delivered before this field existed, including manual resends of those events, are delivered exactly as they were created. Flo does not addpaymentto them retroactively.
A newly created item.purchased for a purchase that took money always includes payment. If that payment is not available yet when Flo prepares the event, Flo retries the event instead of sending it without payment, so an occasional item.purchased can arrive a little later.
Read payment when it is present, and keep your existing invoice or payment.* matching as the fallback when it is absent.
Invoice Payload
{
"id": "7bb5ce14-59e0-41ee-8fb7-84d6967cdb68",
"invoiceNumber": "FLO-1001",
"state": "pastDue",
"billingReason": "subscription_cycle",
"currency": "GBP",
"amountDue": 27,
"amountPaid": 0,
"subtotal": 27,
"total": 27,
"paidAt": null,
"hostedInvoiceUrl": "https://example.com/invoice",
"invoicePdfUrl": "https://example.com/invoice.pdf",
"invoiceDetails": null
}Invoice Event Enrichment
- On invoice events, Flo may include root-level
subscriptionand/oritemalongsideinvoice. - These related objects are resolved from the linked local transaction.
- An invoice event can include either, both, or neither of these related objects.
{
"eventId": "9f6f8b54-8e2d-4f15-8c8a-d7b6d9f41a00",
"eventVersion": "v1",
"objectType": "invoice",
"eventType": "invoice.paid",
"user": {
"id": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"clientUserId": "user_123",
"email": "user@example.com"
},
"invoice": {
"id": "7bb5ce14-59e0-41ee-8fb7-84d6967cdb68",
"invoiceNumber": "FLO-1001",
"state": "paid",
"billingReason": "subscription_cycle",
"currency": "GBP",
"amountDue": 27,
"amountPaid": 27,
"subtotal": 27,
"total": 27,
"paidAt": "2026-04-11T10:20:00.000Z",
"hostedInvoiceUrl": "https://example.com/invoice",
"invoicePdfUrl": "https://example.com/invoice.pdf",
"invoiceDetails": {
"gatewayEventType": "invoice.paid",
"invoiceId": "in_123",
"invoiceStatus": "paid",
"billingReason": "subscription_cycle",
"transactionId": "pi_123",
"currency": "GBP",
"amountDue": 27,
"amountPaid": 27,
"total": 27
}
},
"subscription": {
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"id": "hub-membership",
"name": "Hub Membership",
"description": "Hub membership billed every four weeks",
"quantity": 1,
"currency": "GBP",
"amount": 22,
"total": 22,
"metadata": {
"source": "checkout-subscription"
}
},
"item": {
"uuid": "da634f98-8748-4e90-bb5d-f3df74003b57",
"id": "priority-support",
"name": "Priority Support",
"description": "Priority support add-on",
"quantity": 1,
"currency": "GBP",
"amount": 5,
"total": 5,
"metadata": {
"source": "checkout-item"
}
}
}Invoice-related Subscription Payload
{
"uuid": "d8a2ad28-b98f-4cb6-bf46-f11cc0f5df16",
"id": "hub-membership",
"name": "Hub Membership",
"description": "Hub membership billed every four weeks",
"quantity": 1,
"currency": "GBP",
"amount": 22,
"total": 22,
"metadata": {
"source": "checkout-subscription"
}
}Invoice-related Item Payload
{
"uuid": "da634f98-8748-4e90-bb5d-f3df74003b57",
"id": "priority-support",
"name": "Priority Support",
"description": "Priority support add-on",
"quantity": 1,
"currency": "GBP",
"amount": 5,
"total": 5,
"metadata": {
"source": "checkout-item"
}
}Invoice Details
invoiceDetails is optional on subscription, item, and invoice events. When present, it includes invoice and payment context associated with the Flo event.
Most integrations should build against eventType, objectType, and the explicit resource root (subscription, item, or invoice). invoiceDetails.gatewayEventType is additional trace context and should not be the primary switch for business logic.
{
"gatewayEventType": "invoice.paid",
"invoiceId": "in_123",
"invoiceNumber": "FLO-1001",
"invoiceStatus": "paid",
"billingReason": "subscription_cycle",
"invoicePaymentId": "inpay_123",
"transactionId": "pi_123",
"transactionType": "payment_intent",
"paymentIntentId": "pi_123",
"chargeId": "ch_123",
"paymentRecordId": null,
"receiptNumber": null,
"hostedInvoiceUrl": "https://...",
"invoicePdfUrl": "https://...",
"paidAt": "2026-04-10T12:00:00.000Z",
"currency": "USD",
"amountDue": 29,
"amountPaid": 29,
"subtotal": 29,
"total": 29
}