ACH debit payments

ACH debit lets a US buyer pay once from a bank account. The buyer verifies the account instantly, accepts a debit authorization, and the money arrives days later. On Flint it is the ach_debit payment option on the same PaymentIntent, order, Refund, and Dispute resources you use for cards, with the same webhooks.

Two things set it apart from a card payment. The payment sits in processing for days after the buyer is done, so nothing about the buyer finishing means you have been paid. And a payment that has settled can be pulled back by the buyer's bank, which Flint reports as a dispute.

ACH debit is one shape: a one-time, on-session debit in USD with automatic capture and instant bank verification. The bank account is not saved, and there is no microdeposit fallback for a bank that cannot verify instantly. Keep card available beside it.

How an ACH payment flows#

ACH debit paymentResponse
BrowserYour backendFlintBuyer's bankcreate the PaymentIntent with ach_debit and transaction_purposepayment_collection with instant verification optionsthe buyer verifies the bank, accepts the mandate, and a ConfirmationToken is createdctoken_...POST /confirm with the tokensubmits the debitprocessing, also sent by webhooksettles or rejects the debit, days laterpayment_intent.succeeded or payment_intent.payment_failed
  1. Your backend sends create the PaymentIntent with ach_debit and transaction_purpose to Flint
  2. Flint returns payment_collection with instant verification options to Your backend
  3. Browser sends the buyer verifies the bank, accepts the mandate, and a ConfirmationToken is created to Browser
  4. Browser sends ctoken_... to Your backend
  5. Your backend sends POST /confirm with the token to Flint
  6. Flint sends submits the debit to Buyer's bank
  7. Flint returns processing, also sent by webhook to Your backend
  8. Buyer's bank sends settles or rejects the debit, days later to Flint
  9. Flint sends payment_intent.succeeded or payment_intent.payment_failed to Your backend

Flint Checkout, Payment Links, and the hosted invoice page 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 it through the order, as described in Your own checkout.

Turn on ACH debit#

ACH debit is off by default. Three things have to be in place before a bank debit can be confirmed:

  • The option is enabled for your account. Add ach_debit to the checkout payment options in the dashboard or over the API. Checkout sessions and payment links read this setting. A standalone PaymentIntent names its options in the request, and an invoice uses its payment policy.
  • Your payment account can take bank debits. Include accept_ach_debit_payments in requested_capabilities when you advance onboarding, or ask Flint support to add it to an existing account. Sandbox and live are separate accounts, and each one is activated on its own.
  • Flint has verified two settings on that account. Flint support confirms that the account settles on the standard schedule and that the debit authorization email reaches buyers, and records both for the environment. Until they are recorded, a confirmation with a bank account fails with PAYMENT_OPTION_UNAVAILABLE and a message that asks you to contact support.
cURL
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", "ach_debit"]}}'

GET /v1/capabilities does not report the bank debit capability. To see where an account stands, create a PaymentIntent with payment_options set to ["ach_debit"] in that environment. A PAYMENT_OPTION_UNAVAILABLE error carries the reason that blocks it and capability set to accept_ach_debit_payments. A created PaymentIntent means the option is enabled and the capability is active. The two support-recorded settings are checked when you confirm.

Eligibility#

Flint checks each payment on its own and leaves ACH out, or rejects it, when one of these rules is broken:

  • The currency is USD and the amount is within the limits for the surface.
  • Capture is automatic. ACH is not offered with capture_method set to manual.
  • The payment is one-time and on-session. A ConfirmationToken that asks to save the bank account is rejected with PAYMENT_OPTION_NOT_ALLOWED.
  • No line item on the order tracks inventory. A bank debit stays unresolved for days, and Flint does not hold stock that long, so an order with a tracked line item is card only.
  • The surface offers it: a standalone PaymentIntent, a hosted or embedded checkout session or a payment link backed by an order, or a one-time invoice paid on its hosted page or through an invoice checkout session. Subscriptions, invoice autopay, virtual terminal, and any off-session charge do not offer ACH.

A request that asks for ACH where it is not offered fails with PAYMENT_OPTION_UNAVAILABLE, and the error's reason names the rule: currency_not_supported, manual_capture_not_supported, bounded_inventory_guarantee_not_supported, recurring_ach_not_supported, or source_delayed_settlement_not_supported.

Amount limits#

The minimum is $1.01, so that the $1.00 minimum processing fee stays below the payment. The ceiling depends on where the payment is collected:

SurfaceMaximumRaised by Flint
Standalone PaymentIntent$100,000.00$100,000.00
Hosted checkout session$50,000.00$100,000.00
Invoice$100,000.00$100,000.00
Payment link$25,000.00$50,000.00

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.

Transaction purpose#

Every bank debit records why the money is being taken: goods, services, or other. A standalone PaymentIntent declares it in transaction_purpose, and the value is frozen once the payment exists.

An order decides it from its line items. Any line item sold from a product whose product_type is physical or digital makes the purpose goods. If every line item is a service or fee, the purpose is services. An order with no physical or digital product and a line item that is neither a service nor a fee fails with ACH_TRANSACTION_PURPOSE_UNRESOLVED when the buyer reaches the payment step. Give ad hoc line items a product type, or take ach_debit off that checkout.

Enable ach_debit in the session's payments.enabled_payment_options, or leave the field out to inherit your checkout settings:

cURL
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-ord-1kmn0aExample" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "payments": {"enabled_payment_options": ["card", "ach_debit"]},
    "customer_collection": {"require_email": true},
    "redirects": {"success_redirect_url": "https://shop.example.com/orders/1042/thanks"}
  }'

Flint Checkout offers the bank option only when ACH is available for that order and amount. When it is not, the option is left out and the buyer sees card. When it is, checkout collects a billing name and email, opens instant bank verification, shows the debit authorization, and submits.

After the buyer submits, the payment is processing. Checkout shows a receipt with a "Processing" stamp and a heading such as "Your $25.00 bank payment to Example Shop is processing". A note under it says bank payments take a few business days to clear, that the receipt will be emailed when the payment clears, and that the buyer can close the checkout page. Flint Checkout does not send the buyer to success_redirect_url while the payment is processing.

A payment link uses the same settings and the same lifecycle. A payment link checkout whose payment is processing has not completed the link. The hosted invoice page offers ACH according to the invoice's payment_policy.enabled_payment_options, and the collection attempt stays processing with an expected_settlement_at; Invoicing covers that attempt.

Your own checkout#

A checkout you build on an embedded checkout session offers ACH on the order, the same way it offers card. Enable ach_debit in the session's payments.enabled_payment_options, or leave the field out to inherit your checkout settings, then:

  • Mount the Payment Element from the order's payment_collection, read with the checkout headers. Instant verification and the debit authorization run inside it.
  • Create the ConfirmationToken with the buyer's billing name and email, as in Create the ConfirmationToken below.
  • Pay with POST /v1/orders/{order_id}/pay. The payment attempt stays processing until the bank answers, which takes days, and is not resumable meanwhile. Show the buyer that the payment is processing, and don't start another payment for the order.
  • Fulfill from order.paid, never from the pay response.

ACH needs no return URL, because the buyer never leaves your page. The same steps work for an invoice checkout session launched with "surface": "embedded". Build your own checkout walks through the order flow.

Accept an ACH payment on your own page#

  1. Create the PaymentIntent#

    payment_options must name at least one option. When it includes ach_debit, transaction_purpose is required.

    cURL
    curl -X POST https://api.withflintpay.com/v1/payment-intents \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: ach-order-1042" \
      -d '{
        "amount_money": {"amount": 183600, "currency": "USD"},
        "payment_options": ["card", "ach_debit"],
        "transaction_purpose": "goods",
        "receipt_email": "ada@example.com"
      }'
    

    The response's payment_collection.stripe carries everything the browser needs: the publishable key and connected account_id, and the Elements options, including the bank verification options Flint will confirm with.

    Response
    {
      "data": {
        "payment_intent": {
          "payment_intent_id": "pi_1kmn0aExample",
          "status": "requires_payment_method",
          "amount_money": {"amount": 183600, "currency": "USD"},
          "payment_options": ["card", "ach_debit"],
          "transaction_purpose": "goods"
        },
        "payment_collection": {
          "stripe": {
            "account_id": "acct_Example",
            "publishable_key": "pk_test_Example",
            "elements": {
              "next_step": "create_confirmation_token",
              "submit_to": "confirm_payment_intent",
              "mode": "payment",
              "amount_money": {"amount": 183600, "currency": "USD"},
              "payment_method_types": ["card", "us_bank_account"],
              "payment_method_creation": "manual",
              "payment_method_options": {
                "us_bank_account": {
                  "verification_method": "instant",
                  "financial_connections": {"permissions": ["payment_method"]}
                }
              }
            }
          }
        }
      }
    }
    

    Omitting the purpose returns TRANSACTION_PURPOSE_REQUIRED. Sending it on a PaymentIntent without ach_debit returns TRANSACTION_PURPOSE_NOT_APPLICABLE.

  2. Mount Elements from the collection bootstrap#

    Initialize Stripe.js for the returned connected account and build Elements from payment_collection.stripe.elements as returned. Pass payment_method_options through whole. Stripe requires the options the browser collected with to match the options Flint confirms with, so a rebuilt or trimmed object fails at confirmation.

    JavaScript
    const response = await fetch("/api/payment-intents/ach", {method: "POST"});
    if (!response.ok) throw new Error("Could not create the payment.");
    
    const {data} = await response.json();
    const collection = data.payment_collection.stripe;
    const stripe = Stripe(collection.publishable_key, {
      stripeAccount: collection.account_id,
    });
    
    const elements = stripe.elements({
      mode: collection.elements.mode,
      amount: collection.elements.amount_money.amount,
      currency: collection.elements.amount_money.currency.toLowerCase(),
      paymentMethodCreation: collection.elements.payment_method_creation,
      paymentMethodTypes: collection.elements.payment_method_types,
      paymentMethodOptions: collection.elements.payment_method_options,
    });
    
    elements.create("payment", {layout: "tabs"}).mount("#payment-element");
    

    When the buyer picks the bank tab, the Payment Element runs instant verification and shows the debit authorization. Both happen inside Stripe's hosted flow. There is nothing for you to build there.

  3. Create the ConfirmationToken with the buyer's name and email#

    After elements.submit(), create the token with an accurate billing name and email. Both are required for a bank debit, and the authorization email goes to that address. Send only the token ID to your backend.

    JavaScript
    const {error: submitError} = await elements.submit();
    if (submitError) throw submitError;
    
    const {error, confirmationToken} = await stripe.createConfirmationToken({
      elements,
      params: {
        payment_method_data: {
          billing_details: {
            name: buyer.name,
            email: buyer.email,
          },
        },
      },
    });
    if (error) throw error;
    
    await sendTokenToYourBackend(confirmationToken.id);
    

    A token without a name or email is rejected at confirm with ACH_BILLING_DETAILS_REQUIRED. A token created outside the hosted flow, without the buyer's mandate acceptance, is rejected with ACH_MANDATE_ACCEPTANCE_REQUIRED.

  4. Confirm from your backend#

    cURL
    curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/confirm \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: ach-confirm-1042" \
      -d '{"confirmation_token": "ctoken_Example"}'
    

    Flint submits the debit and answers processing. The bank account the buyer chose is on the payment from this point on.

    Response
    {
      "data": {
        "payment_intent_id": "pi_1kmn0aExample",
        "status": "processing",
        "selected_payment_option": "ach_debit",
        "payment_source": {
          "type": "ach_debit",
          "ach_debit": {"bank_name": "STRIPE TEST BANK", "last4": "6789", "account_type": "checking"}
        },
        "transaction_purpose": "goods"
      }
    }
    

    Do not call a processor confirmation API from the browser, and do not put a payment credential on create or update. Flint is the confirmation authority for both card and ACH.

While the payment is processing#

ACH debit payment lifecycleFinal
  • requires_payment_method moves to processing on confirm
  • requires_payment_method moves to canceled on cancel
  • processing moves to succeeded on the bank settles
  • processing moves to requires_payment_method on the bank rejects
  • succeeded on bank return: arrives as a Dispute

A bank debit stays processing until the buyer's bank answers, and Flint does not time it out. Nothing can be changed in the meantime: cancel returns PAYMENT_INTENT_NOT_CANCELABLE, and an update returns PAYMENT_INTENT_CANNOT_BE_UPDATED. An order whose payment is processing is not paid, and Flint holds the order's payment collection until the outcome is known.

statusWhat it meansWhat to do
processingThe debit was submitted and the bank has not answered.Keep the order unfulfilled. Wait for a webhook or re-read the payment.
succeededThe bank settled the debit.Fulfill once, from the webhook or this read.
requires_payment_methodThe bank rejected the debit, or no bank account was collected yet.Read last_payment_error and collect again with a new token.
canceledThe payment was canceled before a debit was submitted.Stop collecting.

Warning: Fulfill on success, never on processing

A finished checkout and a processing status mean the same thing: the debit has been submitted. Fulfill from payment_intent.succeeded, or from order.paid for an order, and from nothing earlier.

Webhooks#

ACH adds no event types. payment_intent.processing is the one that matters more than it does for a card, because it is the last event you receive for days:

  1. What happens: You confirm with a bank account
  2. Flint sends: payment_intent.processing
    The debit was submitted. Nothing is paid yet.
  3. Flint sends: invoice.payment_processing
    Invoices only. The collection attempt is waiting on the bank.
  4. What happens: The bank settles the debit
  5. Flint sends: payment_intent.succeeded
    The payment settled. processing_fee_money is final.
  6. Flint sends: order.paid
    Checkout sessions, hosted or embedded, and payment links. The order is fully paid.
  7. What happens: The bank rejects the debit
  8. Flint sends: payment_intent.payment_failed
    Carries last_payment_error.
  9. What happens: The buyer's bank pulls a settled payment back
  10. Flint sends: dispute.createdordispute.closedordispute.lost
    All three fire together. The case is final on arrival.

Refunds fire the usual refund.* events. Webhooks covers signature checks and durable delivery.

Failures and retries#

A debit the bank rejects before settling lands the PaymentIntent in requires_payment_method with a Flint-normalized last_payment_error.code. Bank return codes are not passed through.

CodeMeaning
insufficient_fundsThe account could not cover the debit.
bank_account_closedThe account is closed.
bank_account_not_foundNo account matched the details.
bank_debit_not_authorizedThe account holder did not authorize the debit.
bank_account_restrictedThe account cannot accept this debit.
bank_debit_limit_exceededThe debit exceeded the account's limit.
payment_failedThe bank gave no reason Flint maps.

The prior attempt is finished. Create a new ConfirmationToken and confirm again. A standalone integration decides whether to offer the bank option again. Flint does not rewrite payment_options after a failure.

Bank returns#

A payment that settled and is pulled back later is a bank return. Flint represents it with the same Dispute resource as a card chargeback:

  • case_type is bank_return and payment_option is ach_debit.
  • reason is insufficient_funds, bank_debit_not_authorized, bank_account_not_found, or other.
  • status is lost on arrival. evidence_response_allowed and action_required are false, and evidence_due_at is null.

There is no evidence to submit and no outcome to influence. The returned money has already left your balance, so collecting again means a new payment from the buyer. Route these cases to accounting and buyer outreach rather than your evidence queue, and filter on case_type so they never look like work waiting on a deadline. Handling disputes has the full shape.

Refunds#

Refund an ACH payment through the Refunds API as you would a card. Full and partial refunds are both allowed, and a refund does not return the original processing fee.

A bank return reduces what remains refundable, exactly as a prior refund would. A full return leaves nothing to refund, so a later POST /v1/refunds fails with NOTHING_TO_REFUND. A refund and a return that race are serialized so the same money cannot move twice: if the return lands first, the refund is rejected with NOTHING_TO_REFUND, and a refund already in flight fails with failure_reason set to payment_disputed. Treat either as the buyer already having the money back through their bank.

Refunds are funded from your available balance, and a return withdraws from that same balance. When the balance cannot cover a refund, creation fails with REFUND_INSUFFICIENT_AVAILABLE_BALANCE. Restore the balance, then create the refund again with a new Idempotency-Key.

Processing fees#

ACH debit is priced as a percentage of the payment, bounded by a $1.00 minimum and a $700.00 maximum, with no fixed amount. The percentage depends on your plan and is listed in Processing fees. Because settlement is delayed, so is the fee: processing_fee_money is final when the payment succeeds, not when you confirm it. A payment that fails before it succeeds is never charged a processing fee.

Three events around a bank debit carry an event fee instead. It is charged to your merchant account, not deducted from the payment:

EventFee
The buyer's bank account is verified at collection$1.50
The debit fails after Flint submits it to the bank$4.00
A settled payment is returned$15.00

A payment that fails before Flint submits it to the bank carries no event fee.

Test in a sandbox#

Bank debits in test mode use routing number 110000000. Pick the account number for the outcome you need:

Account numberOutcome
000123456789Succeeds.
000222222227Fails for insufficient funds.
000111111113Fails because the account is closed.
000111111116Fails because no account exists.
000333333335Fails because the debit is not authorized.
000555555559Succeeds, then creates a bank return.
000000000009Stays processing.

Sandbox bank accounts verify instantly. Microdeposit account numbers and verification codes do nothing, because microdeposit verification is not offered. Use 000000000009 to confirm that a processing payment leaves the order unpaid, offers no cancel, and that your integration fulfills only after payment_intent.succeeded or order.paid.

Errors#

  • A standalone PaymentIntent must name at least one payment option.
  • payment_options includes ach_debit. Set transaction_purpose to goods, services, or other.
  • Use goods, services, or other.
  • transaction_purpose is only for ACH debit. Remove it, or add ach_debit to payment_options.
  • The order's line items do not say whether the sale is goods or services. Give ad hoc line items a product type.
  • ACH is not available for this payment or account. The reason names the rule, and capability names what to activate. At confirm, the message says which support-recorded setting is missing.
  • The token asks to save the bank account, or collects an option the PaymentIntent did not declare.
  • The amount is under $1.01. Offer card.
  • The amount is over the ceiling for this surface. Offer card, or ask support to raise the limit.
  • Create the ConfirmationToken with the buyer's billing name and email.
  • Collect the bank account through the Payment Element so the buyer accepts the debit authorization.
  • The debit has been submitted. Wait for the outcome, then refund if needed.
  • A bank return already pulled the settled money back, or the payment is fully refunded.
  • HTTP 409
    Your available balance cannot fund the refund. Restore it, then create the refund again with a new Idempotency-Key.

Go-live checklist#

  • ach_debit is enabled in the checkout settings of the environment you are launching.
  • The live payment account has the bank debit capability active, and Flint support has recorded the settlement and authorization email settings for it. Sandbox evidence does not carry over.
  • Checkout and standalone code send a billing name and email with every bank debit.
  • Fulfillment waits for order.paid or payment_intent.succeeded, never a redirect or processing.
  • Your webhook endpoint subscribes to processing, success, failure, dispute, and refund events.
  • Support and accounting know that a successful ACH payment can be returned later, and where bank returns show up.
  • Turning ach_debit off stops new bank debits. Payments already processing still settle, fail, return, and refund through the same webhooks, so keep the endpoint live.

Was this helpful?