Affirm payments
Affirm lets a US buyer split a purchase into installments. The buyer approves a plan on Affirm's site or app, you are paid the full amount up front, and Affirm collects the installments. On Flint it is the affirm payment option on the same PaymentIntent, order, Refund, Return, and Dispute resources you use for cards, with the same webhooks.
Two things set it apart from a card payment. The buyer leaves your page to approve the plan, so every Affirm payment needs a clean return URL, and the browser coming back is not proof of anything. And Affirm approves or declines each purchase on its own, so keep card available beside it.
How an Affirm payment flows#
- Your backend sends create the PaymentIntent with payment_return_url to Flint
- Flint returns payment_collection, including Flint's return_url to Your backend
- Browser sends mount Elements, create a ConfirmationToken with that return_url to Browser
- Browser sends ctoken_... to Your backend
- Your backend sends POST /confirm with the token to Flint
- Flint returns requires_action with a payment_authentication action to Your backend
- Browser sends handleNextAction sends the buyer to Affirm to Affirm
- Affirm sends buyer approves or declines, Affirm returns to Flint's relay to Flint
- Flint returns 303 to your payment_return_url, no query parameters to Browser
- Your backend sends GET the PaymentIntent to Flint
- Flint returns final status, also sent by webhook to Your backend
Flint Checkout and Payment Links run this whole sequence for you. A PaymentIntent your own server creates and confirms follows it step by step, building on the collect-and-confirm flow in Server-confirmed payments. A checkout you build on an embedded checkout session follows the same sequence through the order, as described in Your own checkout.
Turn on Affirm#
Affirm is off by default. Three things have to be in place before you can enable it: Affirm pricing on your merchant plan, an active Affirm capability on your payment account, and complete onboarding requirements. One request tells you which of them is still outstanding:
curl "https://api.withflintpay.com/v1/capabilities?domain=payments&capability=accept_affirm_payments" \
-H "Authorization: Bearer YOUR_API_KEY"
status: "ready" means you can enable Affirm. While it is pending or blocked, each entry in blocked_reasons names the blocker, who resolves it in resolution_owner, and what to do in next_steps.
Once the capability is ready, add affirm to the checkout payment options in the dashboard or over the API:
curl -X PATCH https://api.withflintpay.com/v1/settings \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"checkout": {"enabled_payment_options": ["card", "affirm"]}}'
Enabling Affirm before the capability is ready fails with PAYMENT_OPTION_NOT_READY (409). The error's details list every blocker, and its next action points back at the capability check. Clear the blockers, confirm status: "ready", then enable it.
Enabling Affirm makes it available to the merchant account as a whole. Each payment is then checked on its own for eligibility.
Eligibility#
Flint checks what it can see on the payment and the ConfirmationToken, and rejects a confirmation that breaks one of these rules:
- The currency is USD and the amount is within the limits for the surface.
- Your payment account, the buyer's billing address, and any shipping address are in the US.
- The payment is one-time and on-session. A token that asks to save the payment method is rejected.
- One PaymentIntent collects the full balance. Affirm cannot be one leg of a split payment.
- The surface offers Affirm. Donation, event, and buyer-chosen-amount payment links do not.
Some rules depend on facts Flint never receives, so they stay with you:
- The purchase is consumer commerce, not business to business.
- The goods or services are in stock. Affirm does not allow pre-orders.
- Your business is not in Stripe's prohibited or restricted categories for Affirm.
Keep card available beside Affirm. Affirm can decline a buyer after every Flint check passes, and Stripe or Affirm can review your account after activation.
Amount limits#
Flint's Affirm minimum is $50.00 on every surface. The ceiling depends on where the payment is collected:
| Surface | Minimum | Maximum |
|---|---|---|
| Standalone PaymentIntent, hosted checkout, invoice | $50.00 | $30,000.00 |
| Payment link | $50.00 | $25,000.00, or $30,000.00 with a limit raised by Flint |
A merchant-configured limit can narrow this range but not widen it. An amount outside the range fails at create with AMOUNT_BELOW_LIMIT or AMOUNT_EXCEEDS_LIMIT on amount_money.amount, and the message says which payment option and surface set the limit. The same check runs again at confirm, so an amount that changed between the two calls is caught there.
Where Affirm is offered#
Affirm is available on hosted and embedded checkout sessions, fixed-price payment links, standalone PaymentIntents, one-time invoices collected through the hosted invoice page or an invoice checkout session, and Return balance collection. It is not available for subscriptions or invoice autopay, off-session or saved-credential charges, virtual terminal, or any split payment. A request that asks for Affirm where it is not offered fails with PAYMENT_OPTION_NOT_ALLOWED, and the error's reason says why, for example surface_not_supported, amount_out_of_range, or split_payment_not_supported.
Accept an Affirm payment#
After the return#
The status says what happened at Affirm:
status | What happened | What to do |
|---|---|---|
succeeded | Affirm approved the plan and the payment settled. | Fulfill once, from the webhook or this read. |
requires_capture | Affirm approved a manual-capture hold. | Capture in full before authorization_expires_at. |
requires_action | The buyer has not finished at Affirm, or closed the tab. | Show the pending state. Re-run handleNextAction from the same action if they want to continue. |
requires_payment_method | Affirm declined, or the buyer abandoned or timed out. | Read last_payment_error.code and start a new submission. |
Never create a second PaymentIntent or a second ConfirmationToken just because the browser returned. The buyer may still be inside Affirm's flow, and a lost redirect is recovered by reading the existing payment. Server-confirmed payments has the full recovery order.
Your own checkout#
A checkout you build on an embedded checkout session offers Affirm on the order, the same way it offers card. The differences from a standalone PaymentIntent:
- The return page is the session's
redirects.success_redirect_url, notpayment_return_url. An embedded session that offers Affirm without it fails withEMBEDDED_PAYMENT_RETURN_URL_REQUIRED. The same URL rules apply: HTTPS, no credentials, query string, or fragment, and loopback HTTP in test mode. - Read
payment_collectionfrom the order with the checkout headers. Itsstripe.return_urlis Flint's relay, and the ConfirmationToken must carry that exact URL. - Pay with
POST /v1/orders/{order_id}/pay. The attempt pauses on a pending action that sends the buyer to Affirm, and on return you read the order and the attempt instead of a PaymentIntent. - Fulfill from
order.paid.
This works for an order checkout, an invoice checkout session, and a Return balance launched with "surface": "embedded". Build your own checkout walks through it.
Failures and retries#
A failed Affirm attempt lands the PaymentIntent in requires_payment_method with a normalized last_payment_error.code:
| Code | Meaning |
|---|---|
payment_method_declined | Affirm declined the buyer for this purchase. |
payment_not_completed | The buyer left Affirm without approving a plan. |
payment_action_expired | The redirect was not completed in time. |
payment_method_temporarily_unavailable | Affirm could not take the payment right now. |
payment_method_unavailable | Affirm is not available for this payment. |
The prior attempt is finished. Create a new ConfirmationToken and confirm again, presenting card first. Flint Checkout removes Affirm from the immediate retry after payment_method_declined or payment_method_temporarily_unavailable, and leaves it selectable after an abandoned or expired redirect. A standalone integration decides this itself: Flint does not rewrite payment_options on a PaymentIntent after a failure.
Webhooks#
Affirm adds no event types. It uses the PaymentIntent events, and payment_intent.requires_action fires when the buyer is sent to Affirm:
Fulfill from payment_intent.succeeded, or from order.paid for an order, never from the browser return. Refunds and disputes fire the usual refund.* and dispute.* events.
Manual capture#
Affirm supports "capture_method": "manual" with exactly one full capture. Capture with an empty body. Sending amount_money for anything other than the whole hold returns PARTIAL_CAPTURE_NOT_SUPPORTED, and the hold cannot be raised, lowered, or reauthorized. The hold lasts seven days, and authorization_expires_at carries the deadline. Manual capture covers the hold lifecycle.
Refunds#
Refund an Affirm payment through the Refunds API as you would a card. Full and partial refunds are both allowed. A refund does not return the original processing fee.
Affirm applies the refund to the buyer's loan. It cancels remaining scheduled payments and returns what the buyer has already paid, minus any interest they paid. Two limits come from the provider:
- Submit the refund within 120 days of the payment settling. After that,
POST /v1/refundsreturnsREFUND_SUBMISSION_DEADLINE_EXPIRED, and you reimburse the buyer another way. - The refund is asynchronous and can take up to two days. Wait for
refund.updatedwithsucceeded, orrefund.failed. An Affirm refund cannot be canceled once submitted.
If a refund on an Affirm payment reaches failed, the money is back in your balance and the buyer has not been paid. Reimburse them outside Flint. Any further refund on that payment returns AFFIRM_REFUND_RETRY_NOT_ALLOWED.
Disputes#
An Affirm dispute arrives as the same Dispute resource as a card chargeback, with payment_option set to affirm. A buyer authenticates every Affirm payment by signing in to Affirm, and Affirm covers losses from buyer fraud. Stripe may ask you on Affirm's behalf to pause a shipment before a loss occurs. Comply promptly. Handling disputes covers evidence and deadlines.
Reading an Affirm payment#
Once the buyer picks Affirm, the PaymentIntent identifies it in two fields, and support_reference becomes the Affirm transaction reference:
{
"payment_intent_id": "pi_1kmn0aExample",
"status": "succeeded",
"selected_payment_option": "affirm",
"payment_source": {"type": "affirm"},
"support_reference": "N7XBTQJ4KMEXAMPLE"
}
Give support_reference to a buyer who contacts Affirm support. It is the value Affirm asks for. Before Affirm has issued a reference it holds the PaymentIntent ID, so treat it as an opaque string and do not parse it.
Flint does not risk-assess Affirm payments, so the risk object is absent from an Affirm PaymentIntent. Affirm's own approval decision and your operational controls still apply.
Show Affirm plans before checkout#
Stripe's Payment Method Messaging Element shows the plans a buyer may qualify for on product, cart, and payment pages. Create it on the same connected-account Stripe instance and bind it to the amount you will charge:
stripe
.elements()
.create("paymentMethodMessaging", {
amount: order.total_money.amount,
currency: "USD",
countryCode: "US",
paymentMethodTypes: ["affirm"],
})
.mount("#affirm-message");
The element shows plans that may be available, not an approval. Do not write your own installment amount, APR, or approval claim next to it; Affirm's marketing compliance guides govern that copy. Stripe uses cookies and IP addresses to record which Elements a buyer saw, and you are responsible for disclosing that and obtaining any consent your page needs.
Test in a sandbox#
With a test API key, the same create and confirm calls work, and payment_return_url may be an http://localhost URL. When the buyer picks Affirm and submits, Stripe shows a test page instead of Affirm where you approve or decline the payment. Approve to exercise payment_intent.succeeded, decline to exercise payment_intent.payment_failed and the retry path. There are no Affirm-specific test amounts; any amount within the limits works.
Errors#
Related guides#
- Server-confirmed payments: the collect-and-confirm flow Affirm builds on.
- Build your own checkout: Affirm in an embedded checkout session.
- Declines and payment attempts: recovering an order payment that did not finish.
- Manual capture: holds, capture, and expiry.
- Refunds: the refund lifecycle and failed refunds.
- Handling disputes: evidence and deadlines.
- Webhooks: durable delivery of the events above.
