Fraud, risk, and compliance
Collect address verification data and separate payment authorization from capture without weakening checkout safety.
Fraud, risk, and compliance
Use address verification to give your payment provider better risk signals, and use manual capture when fulfillment must happen only after a separate business decision.
Who this is for
This guide is for developers and operations teams that configure fraud controls, collect billing-address signals, or authorize card payments before inventory, fulfillment, or risk checks are complete.
Choose the control you need
Enable AVS when the provider should compare buyer address data during payment confirmation. Choose manual capture when your server must place a temporary authorization first and capture it only after an approved fulfillment step. These controls can be used independently or together.
Collect address verification data
AVS validates the buyer's billing address against the address on file with their card issuer. When enabled, the SDK collects address fields, forwards them to Stripe as billing_details, and Stripe runs AVS checks automatically. Stricter address checks improve authorization rates and reduce rebill failures.
enableAVS accepts either a boolean (the legacy default) or an AVSFieldConfig object that controls which fields are shown: globally or per country.
Quick Start
The simplest form: country dropdown + postal code, shown for every buyer:
import { FloPayCheckout } from '@flopay/react';
<FloPayCheckout
sessionId="sess_abc123"
enableAVS
onComplete={(result) => console.log('Payment succeeded:', result)}
/>Configurable AVS
Pass an object to control each field independently. Each field accepts:
true: always visiblefalseor omitted: hiddenstring[]: visible only when the buyer's country matches one of these ISO 3166-1 alpha-2 codes
<FloPayCheckout
sessionId={sessionId}
enableAVS={{
country: true, // always show
postal_code: true, // always show
address_line_1: ['US', 'CA'], // street address only for US/CA buyers
address_line_2: ['US', 'CA'], // optional apt/suite line
city: ['US', 'CA'],
state: ['US', 'CA'], // dropdown for US/CA, free text elsewhere
}}
onComplete={handleSuccess}
/>Country codes are normalized (case- and whitespace-insensitive), so ['us', ' ca '] behaves the same as ['US', 'CA'].
Available fields
| Key | Field |
|---|---|
country | Country dropdown (ISO 3166-1 alpha-2) |
postal_code | ZIP / postal code |
address_line_1 | Street address |
address_line_2 | Apt, suite, unit (optional even when visible) |
city | City / town |
state | State / province / region: dropdown for US (50 states) and CA (13 provinces), free text otherwise |
All visible fields are required at submit time, except address_line_2, which is always optional.
Backward compatibility
The boolean form keeps working exactly as before:
| Prop value | Behavior |
|---|---|
enableAVS={false} (or omitted) | No AVS fields |
enableAVS={true} | Country + postal code (the legacy default) |
enableAVS={{ … }} | Custom per-field config |
Visible-Only Data Flow
Hidden fields are never sent to Stripe or to your backend, even if React state was populated earlier (for example, the buyer typed a city, then changed the country to one where city is hidden). Each send site: billing_details, the /process request, and 3DS retries: gates every field through isAVSFieldVisible(config.field, currentCountry).
This means:
- The data you see in Stripe matches the fields the buyer actually saw.
- Country-scoped configs cannot leak data from one country into another.
- Switching country at the form mid-edit clears the now-hidden fields from outgoing payloads automatically.
Where the Address Goes
| Destination | Fields |
|---|---|
Stripe PaymentMethod.billing_details.address | line1, line2, city, state, country, postal_code |
Stripe Customer.address | same six |
Stripe PaymentIntent.shipping.address | same six |
Backend /process request accountData | addressLine1, addressLine2, city, state, country, zip |
Database checkout_session row | user_address_line_1, user_address_line_2, user_address_city, user_address_state, user_address_country, user_address_zip |
3DS Retries Preserve All Fields
When Stripe demands a 3DS challenge after tokenization, the SDK includes the full billing_details object: country, postal code, line1, line2, city, state: on the retry confirmation. The PaymentMethod minted during authentication therefore carries the same address the buyer entered before the challenge.
Pre-fill from GEO/IP
Pass any address fields the partner already knows about the buyer (typically resolved via a GEO/IP lookup or pulled from the partner's user profile) in the session's account object, and the SDK pre-fills the matching AVS inputs:
<FloPayCheckout
createSession={{
clientId: 'your-client-id',
currency: 'EUR',
products: [{ code: 'product-1', totalAmount: 29.99 }],
account: {
userId: 'user_1',
email: 'user@example.com',
// Resolved from GEO/IP / partner profile: any subset is fine.
country: 'CA',
city: 'Toronto',
state: 'ON',
},
successUrl: '/success',
cancelUrl: '/cancel',
}}
enableAVS={{
country: true,
postal_code: true,
address_line_1: ['US', 'CA'],
city: ['US', 'CA'],
state: ['US', 'CA'],
}}
onComplete={handleSuccess}
/>FloPayCheckout automatically forwards session.customer.{country, city, state} from the create-session response into the matching AVS inputs.
Not auto-forwarded (the buyer always types these themselves):
zip/postal_codeaddressLine1/addressLine2
SplitCardForm accepts zip, addressLine1, addressLine2 props for direct consumers, but FloPayCheckout does not pre-populate them. This matches an earlier product decision to avoid steering buyers toward an address Stripe AVS may then reject.
If no country is provided, the country dropdown defaults to US. The buyer can edit any prefilled value before submitting.
Note: Pre-filling does not bypass AVS validation. Stripe still runs the address check against the issuing bank: the prefill just saves the buyer typing.
State Derivation from Postal Code (US / CA)
When the form collects address_line_1 and postal_code but hides the state input, the SDK derives a state / province code from the buyer's postal code and sends it to Stripe billing_details.address.state anyway. This gives Stripe Radar a richer AVS signal without adding another required input.
Trigger conditions (all must hold):
address_line_1is visiblepostal_codeis visiblestateis not visible- Country is
USorCA
When triggered, the SDK calls getStateFromPostalCode(country, postalCode), attaches the result to billing_details.address.state (and to the /process accountData), and the backend persists it onto the session row via an atomic UPDATE … WHERE user_address_state IS NULL. Values the partner already provided at session creation are never overwritten.
Resolution rules:
- US: 5-digit ZIP → 2-letter USPS state code via the 3-digit Sectional Center Facility prefix table.
- CA: A1A 1A1 → 2-letter ISO 3166-2:CA province code via Forward Sortation Area first letter.
Xresolves toNT(shared withNU). - Other countries: returns
null: caller skips state derivation.
import { getStateFromPostalCode } from '@flopay/shared';
getStateFromPostalCode('US', '90210'); // 'CA'
getStateFromPostalCode('US', '10001'); // 'NY'
getStateFromPostalCode('US', '90210-1234');// 'CA' (handles ZIP+4)
getStateFromPostalCode('CA', 'M5V 2T6'); // 'ON'
getStateFromPostalCode('CA', 'V6B1A1'); // 'BC' (compact)
getStateFromPostalCode('GB', 'SW1A 1AA'); // null (not US/CA)City derivation from postal code is not currently supported: Canadian postal codes don't map to a single city, and the US ZIP→city dataset is too large to bundle. If that becomes a priority we'll evaluate a CDN-loaded dataset or a backend-side lookup.
Postal Code Labels
The label adapts automatically based on the selected country:
| Country | Label |
|---|---|
US | ZIP Code |
GB, AU, NZ | Postcode |
CA | Postal Code |
IE | Eircode |
| All others | Postal Code |
The label updates in real time when the buyer changes country.
State / Province Dropdowns
For US and CA buyers, the SDK renders a dropdown populated from US_STATES and CA_PROVINCES. For all other countries, the field falls back to a free-text input labelled by getStateLabel(country) ("State", "Province", "County", "State / Territory", or "State / Province / Region").
import { US_STATES, CA_PROVINCES, getStateOptions, getStateLabel } from '@flopay/shared';
US_STATES.length; // 51 (50 states + DC)
CA_PROVINCES.length; // 13
getStateOptions('US'); // [{ code: 'AL', name: 'Alabama' }, …]
getStateOptions('GB'); // null: render a free-text input
getStateLabel('CA'); // 'Province'
getStateLabel('GB'); // 'County'Field Layout
AVS fields can be arranged side-by-side (default) or stacked:
// Side-by-side (default): country and ZIP share one row when both are visible
<FloPayCheckout sessionId={sessionId} enableAVS onComplete={handleSuccess} />
// Stacked: each field on its own row
<FloPayCheckout
sessionId={sessionId}
enableAVS
avsLayout="column"
onComplete={handleSuccess}
/>When the address fields are visible (address_line_1, address_line_2, city, state), they always render on their own rows above country/postal: buttons-layout still uses the row/column choice for the country and postal pair.
Theming
AVS fields inherit from the card input styles (cardInputBackground, cardInputColor, cardInputBorder). Each AVS field also has a dedicated style slot you can override:
<FloPayCheckout
sessionId={sessionId}
enableAVS={{ country: true, postal_code: true, address_line_1: ['US','CA'] }}
layout="buttons"
theme="bold-dark"
buttonsStyles={{
countrySelect: { backgroundColor: '#1f2937', color: '#f9fafb' },
zipInput: { backgroundColor: '#1f2937', color: '#f9fafb' },
addressLine1Input: { backgroundColor: '#1f2937', color: '#f9fafb' },
addressLine2Input: { backgroundColor: '#1f2937', color: '#f9fafb' },
cityInput: { backgroundColor: '#1f2937', color: '#f9fafb' },
stateInput: { backgroundColor: '#1f2937', color: '#f9fafb' },
}}
onComplete={handleSuccess}
/>The bold-dark and glass-dark theme bundles already include AVS field styles tuned for dark surfaces.
Testing & Selectors
E2E tests can target AVS fields via stable data-testid attributes:
| Field | data-testid |
|---|---|
| Country dropdown | flopay-country |
| Postal / ZIP | flopay-zip |
| Street address | flopay-address-line1 |
| Apt / suite | flopay-address-line2 |
| City | flopay-city |
| State / province | flopay-state |
Props Reference
FloPayCheckout / SplitCardForm
| Prop | Type | Default | Description |
|---|---|---|---|
enableAVS | boolean | AVSFieldConfig | false | Enable AVS. true shows country + postal; an object enables per-field, per-country rules |
avsLayout | 'row' | 'column' | 'row' | Layout for the country/postal pair |
country | string | 'US' | Pre-filled country code (ISO 3166-1 alpha-2) |
zip | string | Not applicable | Pre-filled postal code |
onCountryChange | (country: string) => void | Not applicable | Fires when country changes |
onZipChange | (zip: string) => void | Not applicable | Fires when postal code changes |
ButtonsLayoutStyles (AVS slots)
| Property | Description |
|---|---|
countrySelect | Country dropdown container |
zipInput | ZIP / postcode input container |
addressLine1Input | Street address input |
addressLine2Input | Apt / suite input |
cityInput | City input |
stateInput | State dropdown or text input |
Stripe Radar Configuration
After enabling AVS in the SDK, configure Stripe Radar to act on the results:
- Go to Stripe Dashboard → Radar → Rules
- Add rules based on the AVS check outcome:
Block if ::addressPostalCodeCheck:: = 'fail': block when ZIP doesn't matchBlock if ::addressLine1Check:: = 'fail': block when street address doesn't match (only meaningful whenaddress_line_1is collected)Review if ::addressPostalCodeCheck:: = 'unavailable': review when the issuer doesn't support AVS
AVS coverage varies by issuer and country. Some banks return unavailable for legitimate cards. Blocking on unavailable may reject good payments from regions with limited AVS support.
Shared Helpers
import {
// Types
AVSFieldConfig,
// Resolve / inspect
resolveAVSConfig,
isAVSFieldVisible,
isAVSEnabled,
// Country helpers
getPostalCodeLabel,
COUNTRY_OPTIONS,
getCountryByCode,
// State / province helpers
US_STATES,
CA_PROVINCES,
getStateOptions,
getStateLabel,
} from '@flopay/shared';
resolveAVSConfig(true); // { country: true, postal_code: true }
resolveAVSConfig(false); // null
resolveAVSConfig({ country: true }); // { country: true } (returned as-is)
isAVSFieldVisible(['US', 'CA'], 'us'); // true (case-insensitive)
isAVSFieldVisible(['US'], 'GB'); // false
isAVSFieldVisible(true, 'XX'); // true
isAVSEnabled(true); // true
isAVSEnabled(false); // false
isAVSEnabled({}); // false (no fields configured)
isAVSEnabled({ country: true }); // trueAnalytics
Every checkout records which AVS fields were actually shown (resolved against the buyer's country). The public session parameter types document the AVS exposure, checkout surface, and layout fields available when sessions are created server-side.
Next Steps
- FloPayCheckout API Reference: full props table
- Theming: customize card field appearance
- FloPayCheckout: automatic 3DS handling across the payment lifecycle
- Session parameter types: fields for recording AVS exposure and checkout layout
Authorize now and capture later
Use manual capture when you need to confirm that a customer's funds are available at checkout but collect them only after stock or fulfillment is confirmed. Upgrade every FloPay SDK package used by the checkout to FloPay SDK 1.4.20 or later before opting in.
Authorized funds are not captured funds. An authorized result confirms a temporary hold; it is not payment and must not be recorded as revenue. Do not fulfill the order until capture succeeds and your integration receives item.purchased. Schedule capture or cancellation before authorizationExpiresAt.
If you omit captureMethod, or set captureMethod: 'automatic', immediate capture remains the default. FloPay uses the existing authorize-and-capture checkout behavior and a successful payment reports succeeded.
1. Create an authorization-only checkout
Set captureMethod: 'manual' on an item-only card checkout. The option is supported by Node, browser, detached, inline, and React session creation.
Manual capture preserves the normal cardholder authentication journey. If the issuing bank requires 3-D Secure (3DS), the buyer completes that challenge before the SDK can return authorized. A decline or incomplete authentication never creates a usable authorization.
import { FloPayCheckout } from '@flopay/react';
export function Checkout() {
return (
<FloPayCheckout
createSession={{
clientId: 'your-client-id',
captureMethod: 'manual',
currency: 'GBP',
products: [{ code: 'order_123', quantity: 1 }],
account: {
userId: 'customer_123',
email: 'customer@example.com',
},
successUrl: '/order/accepted',
cancelUrl: '/checkout',
}}
onComplete={(result) => {
if (result.status === 'authorized') {
saveAuthorizationOnYourServer({
paymentId: result.paymentId,
sessionId: result.sessionId,
authorizationExpiresAt: result.authorizationExpiresAt,
});
}
}}
/>
);
}After any required cardholder authentication, the terminal SDK result is:
{
status: 'authorized',
paymentId: '3c54b6ac-7ad5-4e56-9c2c-5a80c2ef40d0',
sessionId: 'fd4475e4-dc19-4439-b830-c9c8f67a35e7',
authorizationExpiresAt: '2026-08-11T14:30:00.000Z',
}Persist paymentId and authorizationExpiresAt with your order. The payment id is a FloPay UUID, not a provider PaymentIntent id.
Authorization expiry
FloPay returns the provider-derived authorizationExpiresAt deadline for that payment. Do not hard-code a universal authorization window: network, card, account, and provider rules can differ. Capture before the returned timestamp, allow operational margin for retries, and treat payment.authorization_expired as terminal.
If the order will not proceed, release the hold rather than waiting for expiry:
Client Basic authentication uses the client UUID as the username and an active user-owned API token as the password. The placeholder below represents that encoded clientUuid:apiToken pair.
PUT /v1/payments/{paymentId}/cancel
Authorization: Basic {base64(clientUuid:apiToken)}
Idempotency-Key: order_123-cancel
Content-Type: application/json
{}2. Capture from a trusted server
When the order is ready to fulfill, call the merchant-authenticated API from trusted server code. Capture is intentionally not an SDK or browser operation.
PUT /v1/payments/{paymentId}/capture
Authorization: Basic {base64(clientUuid:apiToken)}
Idempotency-Key: order_123-full-capture
Content-Type: application/json
{}Use the paymentId returned by the authorized checkout. A successful response is the payment resource with status: 'succeeded'; that is FloPay's captured and paid state.
Keep one stable, nonblank Idempotency-Key for this logical capture. If the response is lost or times out, retry the same request body with the same key. Do not create a fresh key for an uncertain outcome.
If capture fails, FloPay returns 502 Bad Gateway and emits payment.capture_failed. No payment is collected, and the authorization remains inspectable. Keep the order unfulfilled, re-read the payment, then decide whether to retry the same request with the same key or cancel the hold. See the error table.
Fulfill only after capture
For this item-only flow, treat item.purchased as the canonical fulfillment event. FloPay emits it only after the full capture reaches the existing successful-purchase path. There is deliberately no separate payment.captured event. Make your webhook handler idempotent and deduplicate deliveries by eventId.
Supported boundary
The first release supports one later capture for the full amount of an eligible one-time card checkout.
| Capability | Availability |
|---|---|
| Immediate card capture | Supported and the default |
| One later full card capture | Supported with captureMethod: 'manual' |
| Partial capture | Unsupported |
| Incremental authorization | Unsupported |
| Multiple captures | Unsupported |
| Subscription capture | Unsupported |
| Non-card capture, including PayPal and alternative payment methods | Unsupported |
| Dashboard capture as an integration workflow | Unsupported; use the trusted-server API |
Do not use checkoutMode (full, confirm, or auto) to select capture behavior. It controls checkout presentation and saved-method behavior; only captureMethod selects immediate or manual capture.