Save a card and charge it later
A saved card is a payment method that belongs to one customer. The customer enters the card once, in Stripe's card fields on your page, and completes any 3D Secure check their bank asks for. After that, Flint can charge the card without asking for the number again.
How you charge it later depends on whether the customer is there. A one-click repeat purchase runs while they are in your app. An automatic invoice or a subscription renewal runs while they are not, and relies on the authentication they completed when they saved the card.
How saving works#
Saving is its own flow, separate from any payment. Your backend starts it, the browser collects the card, and Flint finishes it when the card processor confirms the setup.
- Your backend sends POST /v1/payment-methods with the customer_id to Flint
- Flint returns a pending payment method and client_setup to Your backend
- Your backend sends account_id, publishable_key, and client_secret to Browser
- Browser sends the customer enters the card, and stripe.confirmSetup runs any 3D Secure check to Browser
- Flint sends payment_method.saved: the card is active to Your backend
confirmSetup succeeding in the browser does not make the card chargeable. The card stays pending until Flint receives the processor's confirmation, usually a few seconds later. Only an active card can be charged or made the default.
Note: Payments save a card only when asked
Order payments and payment links keep no card on file, with two exceptions: subscription signup saves the card that starts the subscription, and a buyer can check "Save my details for faster checkout" in hosted checkout. A card saved in checkout can pay only in later checkouts, not subscriptions or automatic invoices; see Cards buyers save in checkout. To keep a card you can charge without the buyer, run a separate save before or after the purchase.
Before you start#
- A webhook endpoint subscribed to
payment_method.saved,payment_method.failed, andpayment_method.removed. - A page in your app where the customer enters the card. The card fields come from Stripe.js, loaded with keys that Flint returns. You don't need a Stripe account of your own.
- An API key with the scopes shown on these routes:
Only cards can be saved. ACH debit is a one-time payment and does not keep the bank account. Send an Idempotency-Key on every POST and DELETE so a request that times out can be retried safely. See Idempotency.
Save a card#
Card statuses#
- pending moves to active on setup confirmed
- pending moves to failed on bank refuses
- failed moves to active on retry succeeds
- active moves to removed on remove
- active moves to expired on expiration month passes
- expired moves to active on expiration date updated
- pending moves to removed on remove
status on payment methodTreat any status other than active as not chargeable, including values added later.
Cards buyers save in checkout#
Every payment method has a usage that says when Flint may charge it:
usage | Saved by | Can pay |
|---|---|---|
off_session | POST /v1/payment-methods, or a subscription signup | Anything: order payments, checkouts, subscriptions, automatic invoices, and the customer's default |
on_session | The buyer, by checking "Save my details for faster checkout" in hosted checkout | Only checkouts the buyer completes |
A buyer who checks the option saves the card they typed for your business only. Flint saves it after the payment succeeds, for the customer the checkout acts for, and sends payment_method.saved with usage: "on_session". That is the customer you created the session for or, for a guest, the customer whose email the buyer confirmed with a six-digit code Flint emailed them. A typed email alone never saves a card. The next time, checkout emails the buyer a code as soon as they type their email. Once they enter it, checkout lists the cards they saved and pays with one.
A buyer can instead give a US or Canadian mobile phone number with the option: a guest in place of the emailed code, or a buyer who confirmed their email, to save the card with the number. Flint keeps the card for your customer with the buyer's email, as pending, and the buyer confirms it after paying, on the receipt, with a code Flint Pay texts to that number. When the customer existed before the payment, the texted code is enough only if the buyer confirmed the email in the checkout or the number is already the customer's saved number; otherwise the buyer also confirms the email with an emailed code. The card becomes active, with payment_method.saved, once they do. A card not confirmed within 24 hours becomes failed with no event, and no one can pay with it. At the buyer's next checkout, typing their email texts a code to the number.
Each card opens only with the proof it was saved with. A card saved with a number never opens with an emailed code, and a card saved by email never opens with a texted one. A customer has one saved number: a card saved with a new number makes it the customer's number, and the cards saved with the old one are removed, with payment_method.removed for each, since no code can open them anymore. A buyer who lost the phone confirms their email with an emailed code, pays with a card, and saves it with their new number. Save with a mobile phone number covers the API.
The buyer agreed to faster checkout, not to be charged later, so Flint never charges an on_session card without them:
- A subscription, a subscription payment method change, or an automatic invoice returns
PAYMENT_METHOD_ON_SESSION_ONLYfor it. set-defaultreturnsPAYMENT_METHOD_ON_SESSION_ONLY, because subscriptions and automatic invoices charge the default.- Paying an order with it using your API key returns
PAYMENT_METHOD_ON_SESSION_ONLY. Only the checkout session's own credential can spend it.
To list only the cards you can charge without the buyer, filter by usage:
curl "https://api.withflintpay.com/v1/payment-methods?customer_id=cus_1kmn0aExample&usage=off_session" \
-H "Authorization: Bearer YOUR_API_KEY"
Saving in checkout is on by default. Turn it off with checkout.saved_payment_details. Checkout never offers it when customer accounts are merchant hosted, for invoice, subscription, and return checkouts, or for Apple Pay, Google Pay, ACH debit, and Affirm. To build the same option into your own checkout, see Offer to save the card.
Choose a default card#
Flint never picks a default card for you, not even the customer's first card. Set one explicitly:
curl -X POST https://api.withflintpay.com/v1/payment-methods/pm_1kmn0aExample/set-default \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: default-card-ada-001"
The card must be active. A pending or failed card returns PAYMENT_METHOD_NOT_ACTIVE, and a card the buyer saved in checkout, with usage: "on_session", returns PAYMENT_METHOD_ON_SESSION_ONLY. The response is the payment method, and the customer now carries default_payment_method_id: "pm_1kmn0aExample". Setting another card replaces the default. No webhook fires for this change.
The default fills in when a request leaves the card out:
| Charge | Uses the default |
|---|---|
New subscription with no payment_method_id | Yes, at creation. The subscription keeps that card afterward. |
Automatic invoice with no collection.payment_method_id | Yes, when the invoice is issued. The invoice keeps that card afterward. |
| Order payment | No. Pass the card in payment_source. |
Changing the default does not move existing subscriptions or issued invoices to the new card.
A payment method has no "is default" field. To mark the default in your UI, compare each card with the customer's default_payment_method_id, or read the customer with the card expanded. Expanding needs both customers.read and payments.payment_methods.read.
curl "https://api.withflintpay.com/v1/customers/cus_1kmn0aExample?expand=default_payment_method" \
-H "Authorization: Bearer YOUR_API_KEY"
Removing the default card clears default_payment_method_id. Flint does not promote another card, so set a new default if the customer has one.
Charge a saved card#
Pick the path by who is present when the charge runs:
Customer present
Pay an order
A one-click repeat purchase. The customer picks a saved card in your app, sees the total, and confirms. If the bank asks for 3D Secure, they complete it on the page.
Customer not there
Issue an automatic invoice
Usage, an overage, or a balance you bill later. Flint charges the card when you issue the invoice and retries on your schedule if it declines. For the same amount on a schedule, use subscription billing.
The difference is how the charge reaches the bank. Flint sends automatic invoice charges and subscription renewals as charges the customer is not present for, backed by the authentication they completed while saving the card, so the bank can approve them without a challenge. An order payment goes to the bank as a charge with the customer present, and the bank can ask for 3D Secure. If your backend pays orders on a schedule, nobody is there to answer that challenge.
While the customer is present#
Create an order for the customer. With customer_id set, Flint checks that the card you charge belongs to that customer.
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-ada-credits-0922" \
-d '{
"customer_id": "cus_1kmn0aExample",
"line_items": [{
"name": "Credit pack, 1,000 credits",
"quantity": 1,
"unit_price_money": {"amount": 3200, "currency": "USD"},
"fulfillment": {"requirement": "none"}
}]
}'
Show data.settlement_amounts.outstanding_money from the response as the total. When the customer confirms, pay the order with the card they chose:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/pay \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pay-ada-credits-0922" \
-d '{
"action": "pay",
"payment_source": {"payment_method_id": "pm_1kmn0aExample"},
"expected_outstanding_money": {"amount": 3200, "currency": "USD"}
}'
{
"data": {
"order": {
"order_id": "ord_1kmn0aExample",
"payment_status": "paid",
"settlement_amounts": {
"paid_money": {"amount": 3200, "currency": "USD"},
"outstanding_money": {"amount": 0, "currency": "USD"}
}
},
"payment_attempt": {
"order_payment_attempt_id": "opat_1kmn0aExample",
"status": "succeeded",
"is_resumable": false
}
}
}
payment_source.payment_method_idmust be anactivecard withusage: "off_session". Apending,failed, orremovedcard returnsINVALID_PAYMENT_SOURCE. A card that belongs to a different customer returnsPAYMENT_SOURCE_OWNERSHIP_MISMATCH. A card the buyer saved in checkout returnsPAYMENT_METHOD_ON_SESSION_ONLY.expected_outstanding_moneyis the total the customer approved. If tax, shipping, or a promotion changed the balance since, Flint returnsORDER_CHANGED_REFRESH_REQUIREDwithout charging. Show the new total and ask again.- Order payments never fall back to the default card. Without
payment_source, the request fails withPAYMENT_SOURCE_REQUIRED.
Read the result from data.payment_attempt.status, not the HTTP status, because a declined card also returns 200. succeeded means the order is paid. A saved card can still be challenged: the bank may ask for 3D Secure on any charge with the customer present, and the attempt comes back as requires_action. Because the customer is on the page, run the pending action with stripe.handleNextAction and resume the attempt with action: "resume". Finish 3D Secure has the full sequence, and Handle a decline covers failed attempts.
To build the card picker, list the customer's cards and show card.brand, card.last4, and the expiry, with the default preselected when it is still active. Flint does not merge duplicates, so a customer who saves the same card twice has two payment methods. Collapse cards with the same brand, last four digits, and expiry in the picker.
If you would rather not build the picker, a hosted checkout session created for the customer, through the order's customer_id or customer_collection.customer_id, offers that customer's saved cards on the payment page. A session created without a customer offers saved cards only after the buyer confirms their email with a code in checkout; typing the email of an existing customer isn't enough. See Confirm the buyer's email.
When the customer is not there#
Bill the amount with an invoice in automatic collection mode. Flint charges the card as soon as you issue the invoice, sends it as a charge the customer is not present for, and retries on your schedule if it declines.
curl -X POST https://api.withflintpay.com/v1/invoices \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: invoice-ada-usage-sept" \
-d '{
"quick_pay": {
"customer_id": "cus_1kmn0aExample",
"line_items": [{
"name": "API usage, September 2026",
"quantity": 1,
"unit_price_money": {"amount": 4870, "currency": "USD"},
"fulfillment": {"requirement": "none"}
}]
},
"collection": {
"mode": "automatic",
"payment_method_id": "pm_1kmn0aExample"
},
"payment_due": {"type": "none"}
}'
The invoice is a draft until you issue it. caller_managed skips Flint's invoice email when your app sends its own receipt:
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/issue \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: issue-ada-usage-sept" \
-d '{"delivery_mode": "caller_managed"}'
- The card is chosen at issue. Flint uses
collection.payment_method_id, or the customer's default card when you leave it out. With neither, the issue request fails withINVOICE_AUTOPAY_PAYMENT_METHOD_REQUIRED. A card that is notactivefails withPAYMENT_METHOD_NOT_ACTIVE. - The charge runs moments after issue, not inside the issue response. Watch
invoice.paidfor success andinvoice.payment_failedfor a decline. An invoice with a payment schedule charges each installment on its due date instead. - Declines retry on your schedule, set in
invoices.autopay_retry_policy.retry_day_offsetsand fixed for the invoice when it is issued. To try again sooner, callPOST /v1/invoices/{invoice_id}/collectwith a newIdempotency-Key. - A different card can pay an outstanding amount. Pass another active saved card in
payment_method_idwhen callingcollect, or ask the customer to pay by card from the hosted invoice. Future automatic charges still use the card selected at issue.
Automatic collection in the invoicing guide covers the attempt history.
Manage saved cards#
List a customer's cards#
curl "https://api.withflintpay.com/v1/payment-methods?customer_id=cus_1kmn0aExample" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"payment_method_id": "pm_1kmn0aExample",
"customer_id": "cus_1kmn0aExample",
"type": "card",
"status": "active",
"usage": "off_session",
"card": {"brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2030},
"created_at": "2026-09-22T17:04:05Z",
"updated_at": "2026-09-22T17:04:09Z"
}
]
}
The list returns active cards, newest first. Pass status to list another status, such as status=expired for cards past their expiration month or status=pending to find saves the customer abandoned. Pass usage=off_session to leave out cards buyers saved in checkout, for example in a picker for a subscription or an automatic invoice. Page with page_size (up to 100, default 20) and page_token; the last page has no next_page_token. Leave out customer_id to list every saved card in the environment. See Pagination.
Replace the card on a subscription#
A subscription charges the card it was created with, even after the customer's default changes. To move a subscription to a new card, save the card first, then:
curl -X POST https://api.withflintpay.com/v1/subscriptions/sub_1kmn0aExample/payment-method \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: sub-ada-new-card-001" \
-d '{"payment_method_id": "pm_2kmn0aExample"}'
PATCH /v1/subscriptions/{subscription_id} with the same field does the same thing. The card must belong to the subscription's customer, or the request fails with PAYMENT_METHOD_CUSTOMER_MISMATCH. A card that is still pending returns PAYMENT_METHOD_NOT_READY, and a card the buyer saved in checkout returns PAYMENT_METHOD_ON_SESSION_ONLY.
Changing the card does not retry a past_due renewal. Follow up with a payment retry; see When a renewal payment fails.
Remove a card#
curl -X DELETE https://api.withflintpay.com/v1/payment-methods/pm_1kmn0aExample \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: remove-card-ada-001"
{
"data": {"success": true}
}
Removing a card:
- Detaches it from the processor so it can never be charged again, and sets its status to
removed. - Clears the customer's
default_payment_method_idif it pointed at this card. - Sends
payment_method.removed. - Succeeds again, with no second event, if the card is already removed.
A subscription that uses the card blocks removal with PAYMENT_METHOD_HAS_ACTIVE_SUBSCRIPTIONS unless the subscription is canceled. Trialing, paused, and past-due subscriptions all count. Move the subscription to another card or cancel it first.
Open invoices do not block removal, but an automatic invoice issued against the card can no longer collect with it. Void and reissue those invoices for another card.
pending cards can be removed too. Flint does not expire an abandoned save, so it stays pending until you remove it, and a customer deletion request cannot be approved while the customer has any active or pending card.
Card details that change on their own#
When a bank reissues a card, the card network can update the saved card. Flint applies the new brand, last4, exp_month, and exp_year without changing the status or the payment_method_id, and without sending a webhook. Read the card when you display it instead of keeping your own copy of those fields.
Let customers manage their own cards#
If customers manage their cards in an account page you build, call the /v1/me/payment-methods routes with a customer session instead of your API key. They list, read, save, remove, and set a default for the signed-in customer only, and send the same webhooks. Two things differ from the routes above:
- The save request takes no
customer_id. Send the optionaltypefield as you would on the merchant route. GET /v1/me/payment-methods/{payment_method_id}takes noexpand, and answers 404 for a card that belongs to another customer. While a new card ispending, poll it untilstatusisactive.
Build your own customer account walks through that page. Flint's hosted customer account includes card management with no code.
Webhooks#
Every payment_method event carries payment_method_id and customer_id. payment_method.saved adds usage, card_brand, card_last4, card_exp_month, card_exp_year, and card_wallet for wallet cards. payment_method.failed adds failure_code and failure_message when the bank gave a reason. Each event fires at most once per card.
Setting a default card, the passing of a card's expiration month, and a network update to its details send no event. Read the payment method or the customer when you need the current state.
Subscription signup through hosted checkout, embedded checkout, and payment links also saves a card and sends payment_method.saved, so your handler sees cards from every surface. So does a buyer who saves their card in checkout, with usage: "on_session", once they confirm it when they saved it with a mobile phone number. None of these cards become the default.
Test it#
Use a sandbox API key and these cards. Any future expiry and any CVC work.
| Card number | While saving | One-click order payment | Automatic invoice |
|---|---|---|---|
4242 4242 4242 4242 | Saves. | Succeeds. | Succeeds. |
4000 0025 0000 3155 | 3D Secure challenge, then saves. | Challenges again with requires_action. | Succeeds with no challenge. |
4000 0027 6000 3184 | 3D Secure challenge, then saves. | Challenges with requires_action. | Declines because the bank requires authentication. |
4000 0000 0000 0341 | Saves. | Declines. | Declines, then retries on your schedule. |
4000 0000 0000 0002 | Refused. The card becomes failed with card_declined. | Not chargeable. | Not chargeable. |
In the sandbox 3D Secure dialog, choose Fail to rehearse a refused save, then enter 4242 4242 4242 4242 in the same form to watch the same card move from failed to active. Subscription renewals behave like the automatic invoice column. The test card reference lists every Stripe test card, and Testing covers checkout.
A sandbox sends no texts, so a card saved with a mobile phone number is confirmed with a test code. Give any valid US or Canadian mobile phone number, such as +14155552671, then enter one of these codes where the buyer types the texted code:
| Code | Result |
|---|---|
000000 | Wrong code. It counts as a wrong try. |
999999 | CUSTOMER_VERIFICATION_UNAVAILABLE, as when texts can't be checked. |
| Any other six digits | Confirms the code. |
The same codes work for the code texted to a returning buyer. Emailed codes arrive as in live mode.
Errors#
Error handling covers the envelope these arrive in.
Common mistakes#
- Charging the card when the browser confirms. The card is
pendinguntilpayment_method.savedarrives. - Treating
payment_method.failedas final. The customer can fix the card in the same form, and the same card becomesactive. - Assuming the first card becomes the default. Flint never sets a default on its own. Call
set-default. - Paying orders for a customer who is not there. An order payment is sent with the customer present, and a 3D Secure request has nobody to answer it. Use an automatic invoice or a subscription.
- Expecting a new default to move existing charges. Subscriptions and issued invoices keep their own card.
- Offering every saved card for a subscription. Cards buyers saved in checkout have
usage: "on_session"and are refused. List withusage=off_session. - Leaving abandoned saves behind. A
pendingcard never expires, and it blocks a customer deletion request until you remove it.
Next steps#
- Declines and payment attempts: decline codes, 3D Secure, and resuming an attempt.
- Invoicing: automatic collection, retry schedules, and attempt history.
- Subscription billing: charge a saved card on a recurring schedule.
- Customer deletion requests: remove a customer's cards and data on request.
- Payment Methods API reference: every field on every route.
