Documentation
Webhooks

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 same eventId.
  • eventVersion: The webhook schema version.
  • objectType: The top-level Flo resource family for the event: subscription, item, invoice, payment, dispute, or pre_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 only id and clientUserId, 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 on item.purchased as 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 DTOPrimary objectField
WebhookPaymentEventDtopaymentpayment.checkoutMetadata
WebhookItemEventDtoitemitem.checkoutMetadata
WebhookSubscriptionEventDtosubscriptionsubscription.checkoutMetadata
WebhookInvoiceEventDtoinvoiceinvoice.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 use authorized or voided; see the payment state reference.
  • authorizationExpiresAt: Provider-derived capture deadline for an active hold.
  • authorizationVoidReason: merchant_requested, expired, or provider_canceled after a hold becomes voided; otherwise null.

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"
}
FieldTypeMeaning
idstringFlo dispute UUID. Stable across every event for the dispute.
statusenumopen, under_review, won, or lost. won and lost are terminal; see lifecycle and terminal outcomes.
phaseenuminitial for the first round, appeal for a further round after a decision.
actionRequiredbooleantrue while the provider is waiting for your evidence or refund.
responseDueAtstring, nullableISO 8601 deadline for the required action. Set only while actionRequired is true, otherwise null.
amountnumberDisputed amount in major units of currency.
currencystringISO 4217 currency code in upper case.
reasonenumNormalized dispute reason. See dispute reasons.
paymentIdsstring[]Flo payment UUIDs linked to the dispute. Zero, one, or several; empty while unlinked.
linkStatusenumlinked, partially_linked, unmatched, or action_required. See dispute link status.
sequenceintegerIncreases 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.
createdAtstringWhen Flo first recorded the dispute.
updatedAtstringWhen the reported state was observed.

Dispute reasons

reason uses the same values on disputes and Pre-Dispute Alerts:

reasonMeaning
fraud_or_unauthorizedThe cardholder did not authorize or does not recognize the charge.
duplicateThe cardholder was charged more than once for the same purchase.
not_receivedGoods or services were not received.
not_as_describedGoods or services were defective or not as described.
credit_not_processedAn expected refund or credit was not issued.
cancelled_recurringA recurring charge continued after cancellation.
incorrect_amountThe charged amount was wrong.
paid_by_other_meansThe purchase was paid for another way.
processing_errorA processing or bank problem, such as details that could not be processed.
otherAny reason Flo cannot map to the values above.
linkStatusMeaning
linkedEvery provider reference on the dispute resolved to a Flo payment. paymentIds is complete.
partially_linkedSome references resolved. paymentIds holds the linked payments and the rest are queued for review.
action_requiredFlo could not link the dispute automatically and it is queued for review. paymentIds is empty.
unmatchedA 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"
}
FieldTypeMeaning
idstringFlo alert UUID. Stable for the whole alert lifecycle.
statusenumneeds_response, under_review, closed, prevented, or unknown. closed and prevented are terminal.
actionRequiredbooleantrue when you should act. An unknown status fails safe to true.
reasonenum, nullablefraud_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.
responseDueAtstring, nullableISO 8601 deadline for a response. null when no action is required or the source gave no deadline.
amountnumber, nullableAlerted amount in major units of currency.
currencystring, nullableISO 4217 currency code in upper case.
alertAtstring, nullableWhen the alert source raised the alert.
cardLast4string, nullableLast four digits of the card.
merchantDescriptorstring, nullableThe billing descriptor the cardholder saw.
acquirerReferenceNumberstring, nullableThe acquirer reference number of the charge, when the source provided it.
authorizationCodestring, nullableThe authorization code of the charge, when the source provided it.
paymentIdsstring[]Flo payment UUIDs linked to the alert. Empty while unlinked.
linkStatusenumlinked, unmatched, or action_required. See Pre-Dispute Alert link status.
sequenceintegerIncreases per id. It does not start at one and is only comparable within a single id. Discard deliveries that do not advance it.
createdAtstringWhen the alert was first observed.
updatedAtstringWhen 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.

linkStatusMeaning
linkedThe alert's references resolved to Flo payments. paymentIds is authoritative.
unmatchedThe alert carried references, but none matched a Flo payment. paymentIds is empty.
action_requiredThe 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. On item.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:

EventinvoiceDetails.transactionTypeMeaning
item.refundedrefundA genuine merchant or administrative refund.
item.charged_backchargebackMoney 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"
  }
}
FieldTypeMeaning
idstringFlo payment UUID, the ID of the Flo transaction behind the purchase. Never a provider identifier.
checkoutSessionIdstring, nullableThe checkout session the payment came from, or null for a payment without one, such as a Flo-managed renewal.
amountnumberFull payment amount in major units of currency, not the item's share.
currencystringCurrency of the full payment.
statusenumPayment 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.
authorizationExpiresAtstring, nullableISO 8601 capture deadline of a manual-capture authorization hold. null when it does not apply.
authorizationVoidReasonenum, nullablemerchant_requested, expired, or provider_canceled when an authorization hold was voided, otherwise null.
checkoutMetadataobject, optionalThe 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 payment was 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 add payment to 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 subscription and/or item alongside invoice.
  • 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"
    }
  }
}
{
  "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"
  }
}
{
  "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
}

On this page