FloPayFloPay
Guides

AVS (Address Verification)

Configurable Address Verification with per-field, per-country rules. Capture country, ZIP, street address, city, and state to improve auth rates.

AVS (Address Verification)

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 visible
  • false or omitted — hidden
  • string[] — 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

KeyField
countryCountry dropdown (ISO 3166-1 alpha-2)
postal_codeZIP / postal code
address_line_1Street address
address_line_2Apt, suite, unit (optional even when visible)
cityCity / town
stateState / 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.

Backwards Compatibility

The boolean form keeps working exactly as before:

Prop valueBehavior
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

DestinationFields
Stripe PaymentMethod.billing_details.addressline1, line2, city, state, country, postal_code
Stripe Customer.addresssame six
Stripe PaymentIntent.shipping.addresssame six
Backend /process request accountDataaddressLine1, addressLine2, city, state, country, zip
Database checkout_session rowuser_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',
    items: [{ providerItemId: 'product-1', totalAmount: 29.99, currency: 'EUR' }],
    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_code
  • addressLine1 / 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_1 is visible
  • postal_code is visible
  • state is not visible
  • Country is US or CA

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. X resolves to NT (shared with NU).
  • 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:

CountryLabel
USZIP Code
GB, AU, NZPostcode
CAPostal Code
IEEircode
All othersPostal 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"
  buttonsTheme="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 built-in dark theme preset already includes appropriate AVS field styles.

Testing & Selectors

E2E tests can target AVS fields via stable data-testid attributes:

Fielddata-testid
Country dropdownflopay-country
Postal / ZIPflopay-zip
Street addressflopay-address-line1
Apt / suiteflopay-address-line2
Cityflopay-city
State / provinceflopay-state

Props Reference

FloPayCheckout / SplitCardForm

PropTypeDefaultDescription
enableAVSboolean | AVSFieldConfigfalseEnable AVS. true shows country + postal; an object enables per-field, per-country rules
avsLayout'row' | 'column''row'Layout for the country/postal pair
countrystring'US'Pre-filled country code (ISO 3166-1 alpha-2)
zipstringPre-filled postal code
onCountryChange(country: string) => voidFires when country changes
onZipChange(zip: string) => voidFires when postal code changes

ButtonsLayoutStyles (AVS slots)

PropertyDescription
countrySelectCountry dropdown container
zipInputZIP / postcode input container
addressLine1InputStreet address input
addressLine2InputApt / suite input
cityInputCity input
stateInputState dropdown or text input

Stripe Radar Configuration

After enabling AVS in the SDK, configure Stripe Radar to act on the results:

  1. Go to Stripe DashboardRadarRules
  2. Add rules based on the AVS check outcome:
    • Block if ::addressPostalCodeCheck:: = 'fail' — block when ZIP doesn't match
    • Block if ::addressLine1Check:: = 'fail' — block when street address doesn't match (only meaningful when address_line_1 is 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 });       // true

Analytics

Every checkout records which AVS fields were actually shown (resolved against the buyer's country). See the Checkout Analytics guide for the full schema and how to populate analytics when sessions are created server-side.

Next Steps