@flopay/js
Browser-side FloPay SDK. loadFloPay() initializes a FloPay instance for hosted card capture and session access, PaymentAPI is the billing API client, and createCheckoutSession() creates a checkout session and redirects to the hosted checkout page.
Providers sit behind the PaymentProviderAdapter interface (implemented today by StripeAdapter), so checkout code does not change when a session is routed to a different gateway.
Installation
pnpm add @flopay/js
@stripe/stripe-js and @flopay/shared install automatically as dependencies; you do not need to add them. Choose the billing API environment once at startup with configureFlopay() from @flopay/shared, or the NEXT_PUBLIC_FLOPAY_ENV environment variable.
For React applications, @flopay/react builds on this package and renders the complete checkout.
Choosing and upgrading a release
Read the SDK changelog for the target release's changes and
integration guidance before upgrading. When installing more than one @flopay/* package directly,
choose the same released version for each and commit the resulting lockfile. You do not need to
install packages that are already supplied as dependencies.
Confirm that the chosen version is available on npm for every package you are updating. A merged GitHub pull request alone does not mean publication has finished. If that exact version is not yet available, wait for publication rather than mixing versions or installing a repository branch.
Usage
Initialize the SDK
import { loadFloPay } from '@flopay/js';
const flopay = await loadFloPay('pk_test_...', {
billingApiUrl: 'https://billing.example.com', // enables retrieveSession() and retrieveUnifiedSession()
});
The optional second argument accepts every FloPayConfig field except publishableKey: billingApiUrl, locale, appearance, and telemetry. loadFloPay caches by publishable key and behavior-affecting options: concurrent calls with the same key and options share one in-flight provider initialization and resolve to the same FloPay instance. A failed initialization is evicted, so a later call can retry.
Retrieve a session
flopay.retrieveSession(sessionId) returns the normalized CheckoutSession (amount, currency, status, customer). flopay.retrieveUnifiedSession(sessionId) returns a NormalizedCheckoutSession: every gateway the session advertises is listed in providers, and gateway data such as the Stripe client secret is under data. Both use the billingApiUrl from loadFloPay, or a URL you pass as the second argument.
retrieveUnifiedSession() exposes provider-specific session data without
mounting provider UI. Use FloPayCheckout or SplitCardForm from
@flopay/react for hosted-vault card, wallet, and APM rendering.
Call the billing API directly
PaymentAPI wraps the billing API routes the checkout uses. Session reads and continuation calls take the session nonce, the session-bound checkout token returned when the session was created, and send it as x-checkout-session-token.
getUnifiedCheckoutSession(sessionId, nonce)reads and normalizes a session.createSessionIntent(sessionId, nonce, request)creates a Stripe wallet/APM or direct-PayPal intent. The request is a discriminated union of provider, payment-method category, type, and id, and intent kind; direct-card requests are not representable. If the session is still processing an earlier payment (for example a PayPal capture under review), the billing API answers202 checkout_processing. The SDK then polls the session status within the same time limitprocessPaymentuses. It resolves to a settled result,{ settled: true, response }, instead of an intent.responseis theResponseprocessPaymentwould give for that outcome:204succeeded,200authorized,400declined. Check for it withisSettledSessionIntentfrom@flopay/shared. If the time limit runs out first, it rejects withcheckout_processing_timeout,statusCode: 202and thesessionId.reportSessionIntentDecline(sessionId, nonce, request)reports a client-observed non-card decline using a classification code only. Messages, identifiers, card data, credentials, and personal data are rejected before sending.processPayment(userId, params)submits a saved or approved non-card payment to the session's/processroute. When the billing API answers202, the SDK follows its authenticated status endpoint until the latest transaction attempt succeeds, authorizes, declines, requires buyer action, fails, or times out.getPaymentsByEmail(email)looks up prior payments for a saved-payment experience.
Create a hosted checkout session
createCheckoutSession(options) posts to POST /v1/checkouts/sessions and redirects the browser:
- On
201, the browser goes to the hosted checkout page and the result is{ status: 201, redirectUrl, nonce }. A matchingflopay_checkout_tokencookie is written to the same parent domain ascheckout_data, so the hosted page can read the token without putting it in the URL, history, orReferer. - On
204, a payment method is already on file and the browser goes tosuccessUrl.
createCheckoutSessionWithRetries(options) adds automatic retry with jittered exponential backoff on transient transport, timeout, and idempotency-in-progress failures: at most two retries (three attempts), all inside one 12-second total deadline you can change with timeoutMs.
Checkout metadata
Pass checkoutMetadata?: Record<string, string> | null to redirect, retrying, one-shot inline, or detached creation. A present map, an explicit null, and {} are sent unchanged; omitting the field leaves the request body unchanged. Session reads expose a returned snapshot as normalized.checkoutMetadata and normalized.data.session.checkoutMetadata; an omitted or null create value is absent from reads, while {} is returned as {}. The SDK performs no duplicate size validation: billing API rejections keep their code, message, and HTTP statusCode.
Authorise now, capture later
For an item-only card checkout that should place a hold instead of capturing immediately, add captureMethod: 'manual' to createCheckoutSession, createCheckoutSessionWithRetries, PaymentAPI.createAndFetchSession, or PaymentAPI.createDetachedSession. An accepted hold surfaces as status: 'authorized' on normalized sessions and payment results, with paymentId, sessionId, and authorizationExpiresAt. Treat it as checkout-terminal but not paid. Capture is a merchant-authenticated REST operation, never available to browser code; see the payments and authorisations reference.
Idempotent checkout creation
Every checkout-session create sends a stable Idempotency-Key header when a secure random source is available, so a timeout or lost response cannot mint a second session. Existing integrations need no changes.
- Automatic (default): the SDK generates one cryptographically random key per logical
createCheckoutSessioncall and reuses it for every retry of that operation. Independent calls get different keys even when their bodies are identical. The key is never derived from customer data and is never logged or returned. Withoutcrypto.randomUUIDorcrypto.getRandomValuesthe header is omitted; supply your own key to stay idempotent there. - Merchant-supplied: pass
idempotencyKeyto keep the key stable across remounts, SDK instances, or your own server retries. It identifies exactly one logical checkout and must never be reused for a new purchase.
createCheckoutSessionWithRetries resolves the key once, before its retry loop, so timeouts, lost responses, and in-progress replies all replay the same key; a 409 payload conflict is surfaced without retrying. PaymentAPI.createAndFetchSession (used by @flopay/react) follows the same rules, with one shared three-attempt budget across network failures and in-progress replies.
Detached session creation (default)
Inline session creation is detached by default. Instead of one request that resolves buyer identity and the catalog together, the SDK issues two:
POST /v1/checkouts/sessionswithdeferDataAttachment: truereturns a lightweight shell. It runs no catalog validation and takes none of the buyer-identity locks that serialise concurrent checkouts for the same customer, yet already carriesgatewaysand the hostedvaultblock: the card form can mount from it. Checkout metadata is sent on this request.PATCH /v1/checkouts/sessions/{id}/claimattaches products and coupons in the background. When the draft has an email it also attaches the buyer; an email-less draft omits the buyer, and the returned session has its full catalog, totals, and methods whilebuyerIdentifiedstaysfalse.
PaymentAPI.createDetachedSession(params) exposes both phases as { shell, sessionId, nonce, claimed, claim }. claimed settles when the catalog is attached; claim(account) attaches a buyer identity later. Both are independently replay-safe: repeated valid claim(account) calls return the same promise, and a local invalid-email rejection does not consume the operation. createAndFetchSession also accepts an email-less draft and resolves with the catalog-attached anonymous session.
Nothing may be charged before claimed settles: the billing API rejects process, intent, and decline calls on an unclaimed session with 409 checkout_session_data_attachment_required, and holds an unclaimed hosted card charge with a retryable 503. @flopay/react gates the card form's submit button for exactly this window.
For detached creation, timeoutMs is a fresh per-phase budget: the shell POST gets the full value, and after it succeeds the initial claim PATCH gets the full value again under a different AbortSignal (12 seconds each by default), so time spent creating a usable shell never starves its catalog claim. Detached creation requires billing API v1.7.12 or newer and checkoutMode: 'full', and is skipped when tokenizedData is supplied. There is no fallback to the one-shot create; pass deferDataAttachment: false to force it. Because catalog validation moves to the claim, an invalid product or coupon surfaces as the same 422, later in the flow. The claim body is fingerprinted by the billing API, so the SDK replays a byte-identical payload across retries, and a materially different claim for an already-claimed session returns 409 without retrying.
Server-side card gateways (Worldpay)
The billing API can process hosted-vault and eligible saved cards through an approved Worldpay Corporate Gateway entirely server-side. Whether a session can use it is the billing API's decision, made from the merchant's active gateways and the saved method's eligibility. PaymentAPI sends no gateway hint, provider selection, eligibility assertion, merchant code, or scheme reference. Everything stays provider-neutral, so existing integrations change nothing:
- Cards still go through the hosted card form, from the embedded
vaultblock orgetVaultCapture(). There is no Worldpay browser SDK, key, or credential:gateways.worldpayCorporateGatewayhas anullpublishable key and no enabled methods, and it never appears in the normalizedproviderslist. - Outcomes arrive as the same
complete,decline, anderrorevents. Declines use FloPay's decline taxonomy, never a raw processor code. An ambiguous submission stays pending until the billing API reconciles it; the hosted form reports that as anerror, never a success, and a later session read returns the reconciled status. - Cardholder authentication (3DS) resumes through the same provider-neutral continuation as every other card gateway. The hosted form reports
action_requiredwith a Flo-hosted challenge URL, the adapter renders that URL in its own modal overlay, and the outcome is resolved server-side before the hosted form reports the terminalcomplete,decline, orerror. No Worldpay or authentication-provider script, merchant code, ACS URL, or token reaches the browser. - Eligible saved-card Auto, Confirm, returning-buyer, and subscription checkouts call the same provider-neutral
/processroute withouttokenizedData. Immediate success and authorization use the existing Flo shapes; a202 checkout_processingresponse is reconciled through its nonce-protected status URL, including decline, buyer action, and terminal error states. - When a saved method cannot be used,
stored_payment_method_unsupportedis preserved as an actionable Flo error with the instruction to choose another payment method. The SDK never silently switches to a different charging behavior. Scheduled subscription rebills, retries, dunning, and stored-credential eligibility remain billing-API responsibilities.
PaymentAPI Auto creates advertise only the generic async-auto-checkout-v1 response capability so the billing API may return the asynchronous status envelope. It is not a Worldpay capability or routing request; Confirm and Full creates send no capability header. Do not add gateway selection or provider-specific branches to a create request, whether through this SDK or a request you build yourself: the billing API owns that decision.
Hosted card capture
flopay.cardCapture() returns a CardCaptureAdapter that injects the billing API's self-contained hosted card form. The form owns the PCI card fields, its own submit button, tokenization, the charge (created and confirmed server-side), 3DS, and the result, so no Stripe.js runs on the card path and card numbers never enter the SDK runtime. The SDK injects the form and relays its terminal outcome.
- Use the session's embedded
vaultblock when present. Otherwise fetch one withPaymentAPI.getVaultCapture(sessionId, nonce)for a session whose StripeenabledPaymentMethodsexplicitly includescard, or that advertisesgateways.worldpayCorporateGateway.getVaultCapture()shares one active request between concurrent callers, aborts a stalled attempt after ten seconds, and retries transient network failures on the idempotentPOST /v1/checkouts/sessions/{id}/vault/captureroute. - Outcome events are
ready,complete,decline, anderror;challenge_dismissedis a non-terminal notification that the buyer closed the authentication challenge. The form is injected same-window, so the adapter rejects terminal outcomes that are not bound to the mounted session, that mismatch the block'smessageToken, or that arrive from a differentexpectedOrigin. Pass both from the vault block tomount(). - When the buyer's bank asks for authentication, the hosted form hands the challenge up to the adapter, which renders the provider-hosted URL in a full-page modal overlay (
role="dialog",aria-modal, focus moved into the challenge and restored on teardown) above any processing backdrop. The challenge is not an outcome: it reaches no listener, and the challenge URL never appears in an event or in telemetry. The hosted form resolves the outcome and reports it as the usualcomplete,decline, orerror; if nothing comes back at all, the overlay closes itself after the hosted form's own authentication window and reports anerror. Closing the overlay abandons the challenge and reports the non-terminalchallenge_dismissedevent, so the host can take down its processing state and leave the form retryable straight away; it is not an outcome, and a latecomplete,decline, orerrorfrom the hosted form still reaches your listeners. - A
completeevent'sintentIdis the processing gateway's own reference. It is a Stripe PaymentIntent (or SetupIntent) id only when Stripe took the card; for a server-side card gateway it may be an opaque order reference. Reconcile bypaymentIdand the session instead. - For a merchant-server-created no-charge setup session, create the adapter with
operation: 'card_setup'. Acompleteevent may then carry a display-onlypaymentMethod(FloPay id, brand, last four, expiry, and status). capture.applyTheme(theme)re-skins the mounted form without remounting;applyTheme(null)is the explicit classic reset. Theming is cosmetic and never blocks capture.capture.setSubmitLabel(label)sets the hosted submit button's wording live (billing API v1.9.35+), without remounting or clearing entered card data. Pass final, already-localized plain text; the hosted form shows its session default (CONFIRM PAYMENT, orADD CARDfor card setup) for blank or invalid text.capture.setSubmitLabel(null)is the explicit reset to that default, and asubmitLabelmount option sets the initial wording.
The adapter retains the latest label and replays it
every time the widget reports an authenticated ready, so a late or remounted
form still receives it. Because the directive carries merchant text, it is
posted only when the vault block supplied both messageToken and
expectedOrigin, bound to the mounted sessionId, with the exact
expectedOrigin as the postMessage target — never '*':
{ source: 'flopay-vault-host', type: 'submitLabel', sessionId, messageToken, label }
A form mounted without those fields (an older billing API) keeps its default. The label is never added to telemetry. The other host directives (theme, submit gate, and field order) also target the exact expectedOrigin when one was supplied, falling back to '*' only for a widget mounted without it. There is no host directive for authentication: the hosted form accepts no completion signal from the page, because the outcome of a challenge is resolved server-side.
Privacy-safe operational telemetry
Browser telemetry is on by default and best-effort. It reports only the closed SDK error, lifecycle, and performance taxonomy to a FloPay-owned API; it never bundles Sentry, reads Resource Timing, persists a queue, or transmits payment, customer, or form data, credentials, URLs, provider payloads, arbitrary messages, or stacks. Opt out with telemetry: false on loadFloPay, PaymentAPI, PciVaultCardCapture, createCheckoutSession, and createCheckoutSessionWithRetries. Merchant callbacks keep their arguments and invocation order; exceptions they throw are contained and logged only to your console.
Genuine technical failures carry one bounded category: server_error for billing API 5xx responses, transport_error when no usable response arrives, invalid_response for malformed responses, and provider_runtime for provider SDK or hosted-form runtime failures. Payment declines and request validation failures are reported as non-error payment_declined and validation_rejected outcomes.
When telemetry is enabled, a Stripe loader rejection is reported only with the bounded provider_runtime category, while a null Stripe loader result uses the distinct invalid_response category. Neither changes the thrown error or the failed-load retry behavior, and neither transmits the publishable key, rejection, provider message, stack, URL, or provider payload.
FloPayCheckout in @flopay/react also classifies failures around the session and provider boundary without exposing raw errors. An existing session-read transport failure uses transport_error; malformed JSON uses invalid_response.
Filter third-party errors in your own Sentry client
import * as Sentry from '@sentry/browser';
import { dropThirdPartyOnlyError } from '@flopay/js';
Sentry.init({ beforeSend: dropThirdPartyOnlyError });
This pure predicate drops only complete exception stacks with no app or @flopay/* frame and at least one browser-extension URL or Stripe dahlia/stripe.js controller frame. Mixed, unrelated, incomplete, and anonymous-only errors are returned unchanged, and Sentry's in_app flag is ignored.
Examples
Initialize the SDK and read a session's gateways:
import { loadFloPay } from '@flopay/js';
const flopay = await loadFloPay('pk_test_...', { billingApiUrl: 'https://billing.example.com' });
const session = await flopay.retrieveSession('session_uuid');
console.log(session.amount, session.currency, session.status);
const unified = await flopay.retrieveUnifiedSession('session_uuid');
if (unified.providers.includes('stripe')) {
console.log('Stripe publishable key:', unified.data.stripe?.publishableKey);
}
Create a hosted checkout session with metadata and a merchant-controlled idempotency key:
import { createCheckoutSessionWithRetries } from '@flopay/js';
import { FloPayError } from '@flopay/shared';
try {
const result = await createCheckoutSessionWithRetries({
billingApiUrl: 'https://billing.example.com',
checkoutBaseUrl: 'https://checkout.example.com',
clientId: 'client_123',
currency: 'USD',
items: [{ providerItemId: 'prod_abc', providerItemName: 'Pro Plan', totalAmount: 49.99 }],
account: { userId: 'user_1', email: '[email protected]' },
successUrl: '/success',
cancelUrl: '/cancel',
couponCodes: ['SAVE10'],
checkoutMetadata: { merchantOrderId: 'order-123', campaign: 'summer' },
idempotencyKey: 'checkout:order-123',
});
console.log('Checkout session created with status', result.status);
} catch (error) {
if (error instanceof FloPayError && error.code === 'CouponLimitExceeded') {
console.warn('Too many coupon codes.');
} else if (error instanceof FloPayError && error.code === 'CouponCurrencyUnsupported') {
console.warn('A coupon is not valid for this currency.');
} else {
throw error;
}
}
Create a detached session, mount the card form from the shell, and identify the buyer later:
import { PaymentAPI } from '@flopay/js';
const api = new PaymentAPI('https://billing.example.com');
const detached = await api.createDetachedSession({
clientId: 'client_123',
currency: 'EUR',
items: [{ providerItemId: 'prod_abc', providerItemName: 'Widget', totalAmount: 29.99 }],
account: { userId: 'buyer_123' },
successUrl: '/success',
cancelUrl: '/cancel',
});
console.log('Card form can mount from', detached.shell.data.session?.vault);
const anonymous = await detached.claimed; // catalog, totals, and methods; buyerIdentified is false
const identified = await detached.claim({ email: '[email protected]', firstName: 'Jane', lastName: 'Doe' });
console.log(anonymous.data.session?.buyerIdentified, identified.data.session?.buyerIdentified);
Mount the hosted card form and relay its outcome:
import { loadFloPay, PaymentAPI } from '@flopay/js';
import type { PaymentResult } from '@flopay/shared';
const billingApiUrl = 'https://billing.example.com';
const sessionId = 'session_uuid';
const nonce = 'session_nonce';
function onComplete(result: PaymentResult) {
console.log('Checkout finished:', result.status, result.paymentId);
}
const flopay = await loadFloPay('pk_test_...', { billingApiUrl });
const session = await flopay.retrieveSession(sessionId);
const vault = session.vault ?? (await new PaymentAPI(billingApiUrl).getVaultCapture(sessionId, nonce));
const capture = flopay.cardCapture({ sessionId, captureMethod: session.captureMethod });
capture.on('complete', (e) => {
if (e.outcome === 'authorized' || session.captureMethod === 'manual') {
onComplete({
status: 'authorized',
paymentId: e.paymentId,
sessionId: e.sessionId,
authorizationExpiresAt: e.authorizationExpiresAt,
});
return;
}
onComplete({ status: 'succeeded', paymentId: e.paymentId, sessionId: e.sessionId });
});
capture.on('decline', (e) => console.warn('Card declined:', e.declineReason));
capture.on('error', (e) => console.error('Card form failed:', e.message));
const container = document.getElementById('card-form');
if (container && vault.html) {
await capture.mount(container, {
html: vault.html,
messageToken: vault.messageToken,
expectedOrigin: vault.expectedOrigin,
submitLabel: 'Pay €29.99',
});
}
Create a wallet intent and report a client-observed decline:
import { PaymentAPI } from '@flopay/js';
import { isSettledSessionIntent } from '@flopay/shared';
const api = new PaymentAPI('https://billing.example.com', { telemetry: false });
const intent = await api.createSessionIntent('session_uuid', 'session_nonce', {
provider: 'stripe',
paymentMethodCategory: 'wallet',
paymentMethodType: 'apple_pay',
paymentMethodId: null,
intentKind: 'payment',
});
if (isSettledSessionIntent(intent)) {
// An earlier payment on this session already reached its outcome: there is nothing to confirm.
console.log('Session settled with', intent.response.status);
} else if (intent.provider === 'stripe') {
console.log('Confirm the wallet payment with', intent.clientSecret);
}
await api.reportSessionIntentDecline('session_uuid', 'session_nonce', {
provider: 'stripe',
paymentMethodCategory: 'wallet',
paymentMethodType: 'apple_pay',
providerDeclineReason: 'payment_failed',
});
Public API
Exports
| Export | Description |
|---|---|
loadFloPay(publishableKey, options?) | Initializes the SDK and resolves a FloPay. Shares successful and in-flight work by key and options; failed work stays retryable. |
FloPay | Main SDK class, created by loadFloPay() |
PaymentAPI | Billing API client: new PaymentAPI(billingApiUrl, { telemetry }) |
createCheckoutSession(options) | Creates a checkout session and redirects. Resolves a CheckoutSessionResult. |
createCheckoutSessionWithRetries(options) | Like createCheckoutSession, with up to two retries (three attempts) inside one total deadline, using jittered exponential backoff |
PciVaultCardCapture | CardCaptureAdapter implementation that injects the billing API's hosted card form |
CardCaptureOperation | 'checkout' / 'card_setup' |
PciVaultCardCaptureConfig | Adapter configuration: sessionId?, captureMethod?, operation?, expectedOrigin?, telemetry? |
StripeAdapter | PaymentProviderAdapter implementation for Stripe |
toStripeAppearance(appearance) | Converts a FloPayAppearance into a StripeSafeAppearance, mapping FloPay's theme token ('default', 'flat', 'night', 'none') onto Stripe's accepted set. Use it wherever you hand an appearance to Stripe Elements yourself. |
toStripeAppearanceTheme(theme) | Lower-level helper that maps a single FloPay theme token to Stripe's |
StripeSafeAppearance | An appearance Stripe Elements accepts |
cacheSessionDisplayData(sessionId, data, options?) | Stashes display-only session data client-side so it survives the redirect to a checkout page |
getSessionDisplayData(sessionId) | Reads cached display data, or null when none is cached or it expired |
clearSessionDisplayData(sessionId) | Drops cached display data, for example on the success page |
SessionDisplayCacheData | Display-only data stored by cacheSessionDisplayData() |
dropThirdPartyOnlyError(event) | Sentry beforeSend predicate that drops recognized third-party-only exception stacks and otherwise returns the original event |
FloPay methods
| Method | Returns | Description |
|---|---|---|
cardCapture(options?) | CardCaptureAdapter | Creates a hosted card-form adapter bound to sessionId and captureMethod |
retrieveSession(sessionId, billingApiUrl?) | Promise<CheckoutSession> | Reads a session through GET /v1/checkouts/sessions/{id} |
retrieveUnifiedSession(sessionId, billingApiUrl?) | Promise<NormalizedCheckoutSession> | Reads and normalizes a session, including gateway data |
getRawProvider() | unknown | The underlying provider instance, such as the Stripe object |
destroy() | void | Tears down telemetry and the provider |
PaymentAPI methods
| Method | Returns | Description |
|---|---|---|
getUnifiedCheckoutSession(sessionId, nonce?) | Promise<NormalizedCheckoutSession> | Reads and normalizes a session |
getVaultCapture(sessionId, nonce?) | Promise<VaultCaptureBlock> | Fetches the hosted card form when the session has no embedded vault block |
createAndFetchSession(params) | Promise<NormalizedCheckoutSession> | Creates an inline session and resolves once its catalog (and any supplied buyer) is attached |
createDetachedSession(params) | Promise<DetachedCheckoutSession> | Creates an inline session in two phases and exposes both |
claimCheckoutSession(sessionId, nonce, payload) | Promise<NormalizedCheckoutSession> | Sends a PATCH /v1/checkouts/sessions/{id}/claim |
createSessionIntent(sessionId, nonce, request, options?) | Promise<SessionIntentResult> | Creates a Stripe wallet/APM or direct-PayPal intent. If the session is still processing an earlier payment, resolves to { settled: true, response } with that payment's outcome. |
reportSessionIntentDecline(sessionId, nonce, request, options?) | Promise<void> | Reports a non-card decline classification |
processPayment(userId, params, options?) | Promise<Response> | Submits a saved or approved non-card payment to /process; a 202 follows the advertised authenticated status endpoint and maps terminal attempt outcomes back to the existing process response shapes |
waitForCheckoutSessionCompletion(sessionId, options?) | Promise<NormalizedCheckoutSession> | Waits for terminal state; pass the session nonce and a processing response's statusUrl to reconcile attempt-level success, authorization, decline, buyer action, and failure. timeoutMs (15 seconds by default) stops normal polling. A read already in flight may still land during a grace of up to 15 more seconds; if it returns a terminal status, the SDK may start one follow-up session read to normalize the result. The entire operation remains capped by that grace |
reportThreeDsAbandoned(sessionId) | void | Records a saved-card 3DS challenge the buyer never finished as a buyer-abandonment outcome in Flo's privacy-safe telemetry, not as a technical failure. No-op when telemetry is off. The automatic payment button calls it for you |
getPaymentsByEmail(email, options?) | Promise<{ data, total, page, limit }> | Lists prior payments for an email |
destroy() | void | Tears down the client's telemetry |
PciVaultCardCapture methods
| Method | Description |
|---|---|
mount(container, { html, messageToken?, expectedOrigin?, submitLabel? }) | Injects the hosted card form |
on(event, handler) | Subscribes to ready, complete, decline, or error |
unmount() | Removes the form and its listeners |
applyTheme(theme) | Re-skins the mounted form; null is the classic reset |
setSubmitGate(blocked) | While blocked, the hosted form cancels its next submit and emits blocked so the host can validate its own fields (such as AVS) first |
setCardFieldOrder(order, autoFocus) | Reorders the hosted card rows (name, number, expiry); the first row is focused unless autoFocus is false |
setSubmitLabel(label) | Sets the hosted submit wording live; null restores the session default |
Errors
Failures reject with FloPayError from @flopay/shared. Branch on type and code, never on message. When the billing API returns a structured error body, the SDK keeps its safe message, code, and HTTP statusCode; a body-less response falls back to code: 'http_<status>'.
| Code | Type | When |
|---|---|---|
CurrencyRequired | validation_error | Session creation could not resolve a currency from the session, items, subscriptions, or products. Thrown before any request. |
CouponLimitExceeded | validation_error or api_error | More than five coupon codes were supplied: a validation_error before the request, or an api_error from the billing API. |
CouponCurrencyUnsupported | api_error | An amount-based coupon has no price in the cart currency. |
CaptureMethodUnsupportedForSubscription | validation_error | Manual capture was requested for a cart with a subscription. |
InvalidIdempotencyKey | validation_error | A supplied idempotencyKey is blank or longer than 255 characters. Thrown before any request. |
IdempotencyKeyInProgress | api_error | The same key is still being processed. Retried automatically with the same key within the deadline. |
MissingCheckoutSessionToken | validation_error or api_error | A continuation call was made without the session nonce, or the billing API created a session without returning one. |
BuyerEmailCollectionRequired | validation_error | A buyer email supplied for creation or a late claim is not a valid email address. |
checkout_session_data_attachment_required | api_error | A process, intent, or decline call was made before the detached session's catalog claim settled (HTTP 409). Wait for claimed. |
InvalidSessionIntentRequest | validation_error | An intent request is not a supported non-card or PayPal request. |
InvalidAuthorizationAttemptId | validation_error | An intent request's authorization attempt id is not a valid UUID. |
InvalidSessionIntentDeclineRequest | validation_error | A decline report carries anything other than safe classification codes. |
InvalidSessionIntentResponse | api_error | The billing API returned an intent that does not match the request. |
InvalidCheckoutSessionResponse | api_error | The billing API returned a malformed session or no session id. |
session_auto_completed | api_error | Inline creation found a payment method already on file and completed the session without a checkout. |
checkout_processing_timeout | api_error | The SDK could not get a terminal answer in time, so the payment's outcome is not known yet and it may still complete. Do not resubmit or offer the buyer a retry: reconcile from payment.* webhooks or read the session again. The buyer-facing message says the payment is still being confirmed. When createSessionIntent raises it because the session was still processing an earlier payment, it also carries statusCode: 202 and the sessionId. |
stored_payment_method_unsupported | api_error | The backend-selected saved method cannot be used for this transaction. Ask the buyer to choose another payment method; the SDK does not silently change payment behavior. |
vault_capture_timeout | api_error | getVaultCapture() did not return the hosted card form within ten seconds. |
card_capture_no_window | api_error | PciVaultCardCapture.mount() ran outside a browser. |
card_capture_no_widget_html | api_error | PciVaultCardCapture.mount() was called without hosted form HTML. |
Card declines are not thrown: they arrive as the card form's decline event with a declineReason, and technical failures as its error event. A product or coupon rejected during the detached catalog claim surfaces as the billing API's 422 error from claimed.