Payments and authorisations
Payment reporting fields, manual-capture states, capture and cancellation operations, one-off customer charges, authentication, errors, and retry rules.
Payments and authorisations
The payments API exposes one-time card authorisations as a provider-neutral lifecycle. If captureMethod is omitted, or set to automatic, immediate capture remains the default. Manual capture is opt-in and supports one later capture for the full amount.
Financial event fields
Payment resources classify purchases, refunds, and chargebacks explicitly. The following fields are additive; existing response fields are unchanged.
| Field | Type | Meaning |
|---|---|---|
type | purchase / refund / chargeback | Financial event classification. Use this instead of inferring a chargeback from status or provider-specific metadata. |
financialEffectiveAt | ISO 8601 timestamp or null | The authoritative provider money-movement timestamp, when available. |
gatewayChargeId | string or null | The provider charge id that links a refund or chargeback movement to the original purchase. |
For example, a chargeback withdrawal can appear as:
{
"id": "3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0",
"amount": 49.99,
"currency": "GBP",
"type": "chargeback",
"financialEffectiveAt": "2026-08-14T12:05:43.000Z",
"gatewayChargeId": "ch_3Qx9Example"
}Chargeback amounts are signed from Flo's reporting perspective: a positive amount is money withdrawn, while a negative amount is money reinstated after a dispute win. To reconcile Stripe, group Stripe movements by the provider dispute identity retained in metadata and sum their signed amounts. Do not count each lifecycle notification as a separate chargeback.
Chargeback rows never participate in refund aggregation. This reporting contract applies prospectively and does not reclassify or replay historical transaction rows.
Checkout opt-in
POST /v1/checkouts/sessions accepts:
{
"clientId": "18bff186-284c-483f-acee-e712f21d2b8d",
"captureMethod": "manual",
"currency": "GBP",
"products": [{ "code": "order_123", "quantity": 1 }],
"accountData": {
"userId": "customer_123",
"email": "customer@example.com"
},
"successUrl": "https://merchant.example/order/accepted",
"cancelUrl": "https://merchant.example/checkout"
}| Field | Type | Required | Meaning |
|---|---|---|---|
captureMethod | automatic | manual | No | automatic is the default. manual requests an authorisation hold. |
Manual capture is eligible only for one-time card carts. Subscriptions, PayPal, other alternative payment methods, partial capture, incremental authorisation, and multiple capture are unsupported.
After cardholder authentication, GET /v1/checkouts/sessions/{sessionId}/status can return:
{
"data": {
"uuid": "fd4475e4-dc19-4439-b830-c9c8f67a35e7",
"status": "authorized",
"paymentId": "3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0",
"authorizationExpiresAt": "2026-08-11T14:30:00.000Z",
"latestTransactionAttempt": {
"result": "AUTHORIZED"
}
}
}Authenticate the status read with the session's x-checkout-session-token. Save paymentId; capture and cancellation accept the FloPay payment UUID, not a provider intent id.
Payment states
Payment.status is the canonical TransactionStateEnum value:
| Status | Meaning | Money collected? |
|---|---|---|
pending | Created but not yet submitted to a terminal provider outcome. | No |
authorized | Funds are held and capturable before authorizationExpiresAt. | No |
processing | Provider processing is in progress. | Not yet confirmed |
succeeded | Captured and paid. Immediate-capture payments also finish here. | Yes |
failed | Declined, authentication-failed, or otherwise terminally failed. | No |
refunded | The captured amount has been fully refunded. | Previously collected |
partially_refunded | Part of the captured amount has been refunded. | Partially retained |
refund_failed | A refund attempt failed; inspect the payment and refund operation before retrying. | Unchanged |
voided | An uncaptured hold was cancelled or expired. Inspect authorizationVoidReason. | No |
unknown | FloPay cannot classify the provider state yet. Re-read before acting. | Do not assume |
There is no captured, declined, cancelled, or expired payment status. A captured payment is succeeded; a declined authorisation is failed; a cancelled authorisation is voided with reason merchant_requested; and an expired authorisation is voided with reason expired. Provider cancellation can use provider_canceled.
Never treat authorized as paid. Fulfil an item order only after capture returns succeeded and your idempotent webhook handler processes item.purchased.
Capture the full authorisation
PUT /v1/payments/{paymentId}/capture
Authorization: Basic {base64(clientUuid:apiToken)}
Idempotency-Key: order_123-full-capture
Content-Type: application/json
{}The optional JSON body is included in the idempotent request identity. A 200 OK response returns the payment resource. On success its status is succeeded.
Cancel an authorisation
PUT /v1/payments/{paymentId}/cancel
Authorization: Basic {base64(clientUuid:apiToken)}
Idempotency-Key: order_123-cancel
Content-Type: application/json
{}A successful cancellation returns 200 OK with status: 'voided' and authorizationVoidReason: 'merchant_requested'. It releases the hold without collecting funds.
Authentication and permissions
Capture and cancellation are trusted-server operations. They accept Client Basic authentication with the client identifier (client.uuid) as the username and an active user-owned API token as the password. OAuth bearer callers must have a current merchant owner or admin membership. A member or revoked membership receives 403 Forbidden.
The payment is scoped to the authenticated merchant before FloPay resolves provider details. A missing payment and another merchant's payment both return the same 404 Not Found response.
Never expose a user-owned API token or OAuth access token to browser code. The FloPay SDK deliberately has no capture or cancellation method.
Idempotency and retries
Idempotency-Key is mandatory and must be nonblank. Use one stable key per logical operation:
- retry the same operation with the same key and the same request body after a timeout, lost response, or
5xx; - never reuse that key for a different payment, operation, or body;
- the same key with a different body returns
409 Conflictand preserves the original operation; - a durable replay returns the original outcome and never submits a second provider capture.
Capture failure returns 502 Bad Gateway, leaves the authorisation inspectable rather than claiming payment, and emits payment.capture_failed. Re-read the payment before deciding whether another operation is appropriate.
Errors
| HTTP status | When it is returned | Action |
|---|---|---|
400 | Invalid payment UUID, or missing/blank Idempotency-Key. | Correct the request; do not retry unchanged. |
403 | OAuth caller is not a current owner or admin. | Use an authorised server identity. |
404 | Payment is missing or outside the merchant scope. | Treat both cases identically; do not probe provider data. |
409 | Payment is not actively authorised, the hold expired, another operation is active, or the key conflicts with another request. | Re-read the payment and reconcile; do not generate a new key for an uncertain request. |
502 | The gateway could not capture or cancel. | Keep the order unfulfilled, re-read state, and use the same key for a safe transport retry. |
Events
| Event | Meaning |
|---|---|
payment.authorized | A capturable hold is active. Not a fulfilment signal. |
payment.capture_failed | A capture attempt failed and no paid outcome was produced. |
payment.authorization_voided | The merchant released an uncaptured hold. |
payment.authorization_expired | The provider expired an uncaptured hold. |
item.purchased | The capture completed through the existing purchase path; canonical fulfilment event for this item-only flow. |
See Pre-authorisation and capture for the end-to-end task and Webhook events for payloads.
Charge an existing customer
POST /v1/payments creates a one-off payment for an existing FloPay customer, with no checkout session involved. FloPay resolves pricing and coupons against your catalog server-side, then selects eligible saved payment routes. An eligible charge can use Stripe, Worldpay, or PayPal. FloPay chooses the route on the server for each payment; the request has no provider or gateway field.
POST /v1/payments
Authorization: Basic {base64(clientUuid:apiToken)}
Idempotency-Key: order_456-charge
Content-Type: application/json
{
"userId": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"products": [{ "code": "order_123", "quantity": 1 }],
"couponCodes": ["SAVE10"],
"currency": "GBP"
}Supply exactly one of code or uuid per product. Only one-off (Item) products are eligible: subscription products use POST /v1/subscriptions instead. couponCodes is optional.
Idempotency-Key is a required, nonblank header. Retrying the same normalized request with the same key is a safe retry: FloPay resumes or returns the original logical payment instead of submitting a second charge. Reusing the key with a different request returns 409 Conflict and leaves the first payment unchanged.
Both 201 Created and 202 Accepted return { data: PaymentDto }. A 201 response means the purchase is durably succeeded. A 202 response means it is still pending or processing; poll GET /v1/payments/{id} with data.id until the status is terminal. A successful Worldpay charge normally returns 202 first. Retrying the same request with the same key after it settles returns 201 with that payment.
The saved-method decline table in Routing and reliability does not define this endpoint's walk. A Worldpay refusal can recover on another eligible gateway. A card-wide refusal ends the payment at 424; other refusals, including insufficient funds, can move on. A Stripe decline resolves on the account it committed to and does not continue to another gateway. FloPay is aligning this endpoint rule with the Full-checkout walk.
Failure responses use { message, attempts }. A 422 no-route response has an empty attempts array. A 424 response means every chargeable candidate failed definitively and carries ordered, sanitized attempts[]. Each entry identifies both gatewayUuid and paymentMethodUuid, so one payment method can appear more than once when different gateways tried it.
Legacy Stripe Elements and wallet payments do not cascade. There is no separate merchant-facing Full-mode kill switch; per-gateway cascadeEnabled is the only control. New-card first attempts can be divided between processors by share; see Traffic split. See Failover boundaries.
| HTTP status | When it is returned |
|---|---|
400 | The request body is malformed, or Idempotency-Key is missing or blank. |
401 | Client Basic or OAuth bearer authentication is missing or invalid. |
404 | userId doesn't exist, or isn't linked to the authenticated client. |
409 | The Idempotency-Key was already used for a different request. |
422 | The catalog selection, coupons, currency, pricing, or total is ineligible, or no eligible route exists. A no-route response has empty attempts. |
424 | Every chargeable candidate failed definitively. See the ordered attempts[]. |
See Routing and reliability for the separately scoped saved-method and Full-checkout rules, and Manage saved cards safely for getting a customer to a chargeable state first.