Save and manage payment methods
Let customers add, list, replace, and remove saved cards, wallets, PayPal, and bank methods without a purchase, from trusted server and browser boundaries.
Save and manage payment methods
Give customers a payment-methods page where they can add the ways they pay, see what is on file, replace a method, and remove one, without making a purchase. FloPay decides which methods can be saved, renders them in a secure surface, verifies each one with the provider, and keeps the reusable credential away from your servers.
Who this is for
This guide is for checkout developers and backend teams building a customer account area for saved payment methods of every type: cards, wallets, PayPal, and bank methods. It builds on the card-only flow in Manage saved cards safely, which stays unchanged.
How the pieces fit together
Saving a payment method deliberately splits authority three ways:
- Your trusted server resolves the customer, creates setup sessions, lists saved methods, and removes them. Your merchant credentials, the client UUID and API token, stay server-side and are never sent to a browser.
- The browser receives only a setup
sessionIdandnonce, and rendersFloPayPaymentMethodSetupfrom@flopay/react. - FloPay decides which methods the customer can save, verifies the chosen method with the provider, and records it as a saved method only once it is verified.
Card data never reaches your application servers or FloPay's application servers: the card is entered in FloPay's hosted, PCI-compliant card form and the reusable credential stays in the vault. Wallets, PayPal, and bank methods are approved in the provider's own surface, such as the device wallet sheet, a PayPal approval, or the provider's mandate form.
Client Basic authentication uses the client UUID as the username and an active user-owned API token as the password. In the examples below, {base64(clientUuid:apiToken)} represents that encoded pair.
Which methods can be saved
FloPay decides which methods a customer can save, per merchant and customer context: from your connected gateways, their current eligibility, and the resolved country and currency, using the same eligibility policy as checkout. The setup session advertises the result, and the add-payment-method surface renders only what the session advertises. Your code never decides it, and there is no list of methods to configure in the SDK.
These are the methods FloPay can advertise on a setup session today:
| Method | What the customer does | What it needs |
|---|---|---|
| Card | Enters the card in FloPay's hosted card form. | A connected card gateway. |
| Apple Pay | Approves in the Apple Pay sheet. | A device and browser with Apple Pay. |
| Google Pay | Approves in the Google Pay sheet. | A device and browser with Google Pay. |
| SEPA Direct Debit | Enters an IBAN and accepts the mandate. | A EUR session. |
| US bank account | Chooses a bank or enters account details. | A USD session. |
| PayPal | Approves in PayPal. | A connected PayPal gateway. |
Not every method offered at checkout is savable:
- One-off redirect methods authorize a single payment and leave nothing the provider can charge again.
- Buy-now-pay-later methods make a fresh credit decision for each purchase, with no reusable mandate.
- Any other method without a reusable setup path is in the same position until FloPay adds one.
FloPay hides methods that cannot be saved instead of showing them disabled, so every method the customer sees on the add-payment-method surface can actually be saved. A merchant with only card gateways sees exactly the card form. FloPay adds methods to setup one at a time as each gains a reusable setup path. Until a method is advertised, nothing in your integration changes, and when it is, it appears without a code change.
What a saved method can be charged for
A setup session never charges anything. What FloPay can do with the method afterwards depends on its type:
| Saved method | Used for later payments and subscription rebills |
|---|---|
| Card | Yes. |
| Apple Pay, Google Pay | Yes, exactly like a wallet the customer paid with at checkout. |
| PayPal | Yes. |
| SEPA Direct Debit, US bank account | Not yet. The method is saved, listed, and removable, and FloPay does not charge it. |
A saved Apple Pay or Google Pay wallet joins the customer's cards: FloPay charges the most recently used method first and falls back to the others, and a subscription can renew on it.
A wallet is charged without the customer present only when the setup session was created for off-session use, which is the default. A wallet saved with futureUse: "on_session" is listed and is not charged. Do not build charging around a saved bank method yet.
1. Record consent and disclosure
Before starting setup, tell the customer what saving the method means and obtain their affirmative consent for future off-session charges under your terms. Retain the consent text and version, the customer identity, the timestamp, and your business purpose. The setup session and the provider's verification outcome are technical evidence; they do not replace your consent record.
FloPay's hosted card form always shows the card-storage disclosure below its submit button, with a link to the Card Storage Policy, and it cannot be removed or replaced. That disclosure does not replace yours: you keep the responsibility for your own card-storage disclosure, for the mandate and consent terms of any method you let customers save, and for collecting and retaining the customer's agreement to future off-session charges.
2. Resolve the customer
The setup and list APIs use FloPay's customer UUID. If you store your own customer id, resolve it from your trusted server:
GET /v1/users?clientUserId=customer_123
Authorization: Basic {base64(clientUuid:apiToken)}Read the first exact match's id from the paginated data array. An empty array means there is no customer linked to this merchant. Never accept a FloPay customer UUID supplied directly by a browser.
3. Create a setup session
Create the no-charge setup session from the same trusted server. It is the same endpoint card setup uses, extended additively: existing card-only integrations keep working unchanged.
POST /v1/checkouts/sessions/setup
Authorization: Basic {base64(clientUuid:apiToken)}
Content-Type: application/json
{
"userId": "3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7",
"successUrl": "https://merchant.example/account/payment-methods",
"cancelUrl": "https://merchant.example/account/payment-methods",
"country": "GB",
"currency": "GBP",
"futureUse": "off_session"
}userId is the resolved FloPay UUID, and the customer must be linked to the authenticated merchant. Unknown and cross-merchant customers both return 404 Not Found before FloPay evaluates any gateway or contacts a provider.
The remaining fields are optional, independent, and nullable:
| Field | Type | Meaning |
|---|---|---|
successUrl | string | Destination recorded on the session for a verified setup. |
cancelUrl | string | Destination recorded on the session for an abandoned setup. |
country | ISO 3166-1 alpha-2 | The country the method will be used in. |
currency | three-letter code | The currency later payments will be charged in. |
futureUse | on_session or off_session | How the saved method will be reused. Omit it, or send null, and the method is set up for off-session use. Send on_session only for a method that will be used only while the customer is present. |
Send the values you already hold: FloPay owns normalization, validation, and defaults, and may fill an omitted country or currency from this customer's own history with you. FloPay uses country and currency as context when deciding which methods to advertise, but the context never selects or hints a gateway: there is no field, header, or SDK setting that chooses one. See Gateway selection.
Send currency whenever you know it. Wallets and bank methods are offered in the session currency, and some only exist in one: SEPA Direct Debit needs EUR and a US bank account needs USD. When you send none and FloPay cannot resolve one from the customer's history, the session uses the currency of the card gateway it selected, and a method that does not support that currency is not offered.
@flopay/shared publishes the body as the type-only CreateCardSetupSessionRequest, so you can type the call you make from your server. @flopay/node has no setup method by design, because it would need the merchant credential this call requires:
import type { CreateCardSetupSessionRequest } from '@flopay/shared';
const authorization = `Basic ${Buffer.from(
`${process.env.FLOPAY_CLIENT_UUID}:${process.env.FLOPAY_API_TOKEN}`,
).toString('base64')}`;
export async function createSetupSession(floPayCustomerUuid: string) {
const body: CreateCardSetupSessionRequest = {
userId: floPayCustomerUuid,
successUrl: 'https://merchant.example/account/payment-methods',
cancelUrl: 'https://merchant.example/account/payment-methods',
country: 'GB',
currency: 'GBP',
};
const response = await fetch('https://api.flopay.com/v1/checkouts/sessions/setup', {
method: 'POST',
headers: { Authorization: authorization, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Setup session request failed with status ${response.status}`);
}
const { data } = await response.json();
// Return only the opaque session id and nonce to the browser.
return { sessionId: data.uuid as string, nonce: data.nonce as string };
}201 Created returns the checkout-session shape with checkoutMode: 'setup', no products, and no amount, together with the methods this session can save (advertisedPaymentMethods) and its currency. The component reads both for you, so your server only needs to hand over the session id and nonce. Do not forward the merchant Authorization header, and do not call the purchase process endpoint for a setup session.
4. Render the add-payment-method surface
Mount FloPayPaymentMethodSetup with the session id and nonce. It renders every method the setup session advertises and saves the customer's choice without a purchase. A card-only session behaves exactly like FloPayCardSetup.
'use client';
import { FloPayPaymentMethodSetup } from '@flopay/react/payment-method-setup';
import { useState } from 'react';
type AddPaymentMethodProps = {
sessionId: string;
nonce: string;
onSaved: (savedCardId: string | undefined) => void;
onLeft: () => void;
};
export function AddPaymentMethod({ sessionId, nonce, onSaved, onLeft }: AddPaymentMethodProps) {
const [message, setMessage] = useState<string | null>(null);
return (
<>
<FloPayPaymentMethodSetup
sessionId={sessionId}
nonce={nonce}
onComplete={(event) => {
setMessage(null);
// Present for a card. List the methods again from your server for every type.
onSaved(event.paymentMethod?.id);
}}
onDecline={(event) => {
setMessage(event.message ?? 'That payment method could not be verified. Try another one.');
}}
onCancel={() => onLeft()}
onError={(error) => {
// Keep the form mounted on a retryable error: the customer can try again.
setMessage(error.retryable ? 'Something went wrong. Please try again.' : error.message);
}}
/>
{message ? <p role="alert">{message}</p> : null}
</>
);
}Import from @flopay/react/payment-method-setup on a page that only saves methods: it loads a provider's script only when the session advertises one of that provider's methods. The component is also exported from @flopay/react.
The component is browser-only, needs nothing but sessionId and nonce, and does not need FloPayProvider. It also accepts theme, containerStyle, loading, billingApiUrl, telemetry, onReady, and onValidation, as listed in the FloPayPaymentMethodSetup reference.
What the customer sees
The form is laid out the way FloPayCheckout lays out a purchase, using the same components, so customers meet one familiar surface:
- Wallet buttons, such as Apple Pay and Google Pay.
- PayPal.
- One tile for each bank method. Pressing a tile opens that method's form with a
Savebutton. - An
or add a cardrule. - FloPay's hosted card form.
A method the provider cannot offer in this browser is left out without an error: a wallet the device does not have, or a method that does not support the session currency. That is why two customers on the same session settings can see different wallets. Do not render your own list of offered methods beside the form: it can name a method the form has correctly left out.
5. Handle the setup outcomes
Each callback reports one outcome. Treat only completion as success:
| Callback | Outcome | What to do |
|---|---|---|
onComplete | Completion: FloPay reports the method verified and usable. | List the methods again from your server. event.paymentMethod carries a saved card's display fields, including its id. It is absent for a wallet, PayPal, or a bank method. |
onDecline | Decline: the provider terminally declined verification. Never paired with onComplete. | Show event.message or your own wording, and let the customer try another method. Nothing was saved. |
onCancel | Cancel: the surface unmounted before a terminal outcome. Fires once. | Return the customer to the list and list the methods again. |
onError | Error: session validation, transport, provider, or card-form failure. | Use error.retryable to keep the form open for a retry, or create a new setup session. |
Do not treat mount, submit, or a provider redirect as success. A declined, abandoned, redirect-incomplete, or authentication-incomplete setup never creates an active method and never changes an existing one.
Completion is FloPay's decision. After the customer approves a wallet, PayPal, or a bank method, the form shows SAVING PAYMENT METHOD... and reads the setup session until FloPay reports it completed, then fires onComplete. Some bank methods are verified by the provider later: the method stays pending until then and becomes active without any further call from you. A customer who leaves during that wait triggers onCancel, so always treat the list from your server as the source of truth.
The error codes you are most likely to handle:
error.code | Retryable | What it means |
|---|---|---|
payment_method_setup_unverified | Yes | The provider accepted the method, but FloPay had not verified it before the form stopped waiting. The method may still become active, so list the methods again before asking the customer to retry. |
payment_method_setup_unavailable | No | None of the methods this session advertises can be offered in this browser. |
NoAdvertisedPaymentMethods | No | The session advertises nothing the form can render. |
payment_method_setup_expired | No | The setup session expired. Create a new one. |
popup_blocked | Yes | PayPal could not open its approval window. Ask the customer to allow pop-ups. |
The complete list is in the FloPayPaymentMethodSetup reference.
Returning from a provider redirect
Some methods send the customer to the provider and back to the page they left. To finish that setup, mount FloPayPaymentMethodSetup again with the same sessionId and nonce when the customer returns. It reads the provider's return parameters from the URL, removes them, and reports the outcome through the same callbacks.
Keep the open setup's sessionId and nonce somewhere that survives the navigation, such as the tab's session storage, and clear them once a callback reports an outcome.
6. List saved methods with the type filter
List the customer's saved methods from your trusted server. Add status[eq]=active for methods the customer can use, and type[eq]= to show one type at a time:
GET /v1/payment-methods?userUuid=3b4d9a11-0ce8-4a88-9cb1-b4f43d03d2b7&type[eq]=card&status[eq]=active
Authorization: Basic {base64(clientUuid:apiToken)}type takes the method type: card, paypal, apple_pay, google_pay, sepa_debit, or us_bank_account. type[in]=card,apple_pay returns several types at once, and any other value returns 400. Leave type out to list every saved method together.
Every row has the same envelope, and two display fields tell you what to show:
| Row | Show |
|---|---|
A card (methodType: "card") | brand, lastFour, expiryMonth, and expiryYear. displayLabel is null. |
| PayPal | displayLabel, which is PayPal with a masked account email. |
| A wallet or bank method | displayLabel, such as Apple Pay or SEPA Direct Debit. |
Card fields are null on every row that is not a card. Read methodType defensively and show a row's displayLabel for a type your interface does not recognize yet, instead of hiding the row. The full response is in the payment methods REST reference.
The API never returns provider secrets, client secrets, vault or provider tokens, provider payment-method ids, or full account details such as a card number, CVC, or bank account number. Keep each method's FloPay id on your server for later removal. Customers may hold several active methods, including several of the same type.
7. Replace a method
There is no replace endpoint. Replace is a composition of the operations above:
- Complete setup for the new method.
- Confirm that the exact new method is
activein the list. - Call
DELETE /v1/payment-methods/{id}for the old method.
onComplete carries a new card's id in event.paymentMethod. For any other method it does not, so do not guess the new method from the newest row: a setup on another device can make that pick the wrong method. Leave the old method in place and let the customer remove it from the list themselves.
If setup fails, leave the old method unchanged. When the old method funds a subscription, FloPay moves that funding only to an eligible active method of the same type, verifies the move, and only then removes the old method, so a card is never replaced by a wallet or the reverse. If deletion returns a conflict or a gateway error, keep the old method visible and explain the next step; never hide it optimistically.
8. Remove a method
DELETE /v1/payment-methods/{id}
Authorization: Basic {base64(clientUuid:apiToken)}| Status | Code | Customer-facing handling |
|---|---|---|
204 | none | Removal finished, or an owned removal had already finished. The operation is idempotent, so list the methods again. |
404 | payment_method_not_found | Show a generic "payment method not found" message. Missing and cross-merchant methods are deliberately indistinguishable. |
409 | payment_method_has_active_payment | Ask the customer to wait for the active payment attempt to finish. |
409 | payment_method_in_use | A subscription depends on this method and has no eligible replacement. Ask the customer to add and verify another method of the same type first. |
409 | payment_method_reassignment_requires_payer_action | PayPal funding needs an interactive payer authorization. Do not imply that adding a card resolves it. |
502 | payment_method_deletion_failed | Keep the method visible. Follow retrySafe and resolution; repeating the same DELETE is idempotent. |
A 409 leaves the method exactly as it was. Removed methods remain as sanitized tombstones with status deleted, so filter on status[eq]=active for your list. Payment, refund, and audit history stay intact.
Security checklist
- Create setup sessions, list methods, and remove methods only on your trusted server. Merchant credentials stay server-side.
- Hand the browser only
sessionIdandnonce. - Treat only
onComplete, confirmed by anactiverow in the list, as a saved method. - Show only display fields. Never log or return raw card or bank data.
- Treat every
404alike and do not probe whether another merchant owns an id. - Keep your disclosure and the customer's consent for future off-session charges.
See the FloPayPaymentMethodSetup reference for the component's props, events, and errors, and the payment methods REST reference for the complete request, response, and error contract.