I went looking for how to take bank payments in a side project and came back with a list of things Stripe won't let a new account do yet.
Stripe's guides are genuinely good at "here are the payment types." They're much quieter about the second list: what your four-month-old account isn't eligible for, and which of today's choices you can't reverse once a customer has agreed to them. That's the list that would have changed how I designed this, so it's the one I wrote down. Everything below is from Stripe's own documentation, re-checked before publishing.
Why anyone leaves cards in the first place
Stripe's standard US rate for online domestic cards is 2.9% + 30 cents. ACH Direct Debit is 0.8% per transaction, capped at $5.00.
On a $50 charge that's $1.75 versus $0.40. Real, but not interesting. On a $2,500 charge it's $72.80 versus $5.00.
The cap is the whole story. 0.8% reaches $5.00 at $625, so every charge above that costs exactly five dollars no matter how large it gets. Below a few hundred dollars, the rail you pick barely registers. Above it, the rail is a line item.
That's the honest reason a side project ends up reading bank debit docs: not ideology, arithmetic.
The four options, and what each one thinks it owns
Stripe's recurring payments guide lays out the menu. Reframed as "what does this primitive believe it's in charge of":
- One-time Checkout. A single payment, with no opinion about what happens next.
- Subscriptions (Billing). A schedule.
- Standalone Invoicing. A due date and a small ledger.
- Off-session PaymentIntents against a saved payment method. One money movement, nothing else.
The second one is the insight worth having. A Subscription is a schedule: Stripe generates an invoice for each billing cycle, anchored to a billing_cycle_anchor you set at creation. If your app already knows when a customer owes you money, you now have two schedules, and two schedules can disagree. Renewal price changes pull in proration you already computed yourself, a payment recorded outside Stripe leaves an open invoice Stripe keeps chasing, and the subscription's status machine starts making assertions about a customer's account that your database thinks are its own call.
Two smaller edges: partial payments aren't supported on subscription invoices when collection_method is charge_automatically, and Stripe's product support matrix marks the Customer Portal as unsupported for ACH Direct Debit, so the hosted self-serve billing page doesn't cover bank accounts.
What a new account cannot do yet
Standard ACH settlement is T+4: four business days from payment creation, 21:00 US/Eastern cutoff. There's a faster T+2 option, and this is the constraint nobody puts on a landing page. From Stripe's support page:
"US businesses in good standing will typically be eligible for two-day settlement after processing on Stripe for a minimum of 120 days."
Read the hedges rather than the number. "Typically" and "a minimum of" mean 120 days is a floor, not a promise. The same page adds that if your account stops meeting the requirements, "your eligibility may be revoked and your processing will revert back to standard timing."
So for at least four months you're on T+4, and after that it's a privilege that can be withdrawn. Any design where the fast path is load-bearing doesn't work for your first four months and might stop working later. Build for T+4.
The other new-account constraint is volume. Stripe's support page confirms bank debits are subject to limits on both transaction size and weekly volume, and that Stripe reviews and adjusts them based on account history and risk factors. It publishes no numbers. Several third-party help centers quote specific daily and weekly figures; I'm not repeating them, because they're nowhere in Stripe's own documentation.
What Stripe does give you is a test account number whose entire purpose is simulating this: 000777777771 fails "due to payment amount causing the account to exceed its weekly payment volume limit." A failure mode with a dedicated test fixture is one you should handle, even when the threshold is undocumented.
Succeeded is not final
Four business days to settle means a payment spends real time in a state that is neither paid nor failed. That much you can plan for. What catches people is that the end state isn't final either.
"In rare situations, Stripe might receive an ACH failure from the bank after a PaymentIntent has transitioned to
succeeded."
It comes back as a dispute. So there are three outcomes to model rather than two: processing, succeeded, and reversed-after-success. A mutable status column handles the first two and quietly loses the third.
Disputes here are heavier than the word suggests. An ACH dispute is final and uncontestable, and it invalidates the mandate, so you can't reuse that payment method. "If they dispute a subsequent payment, Stripe blocks the bank account from further re-use." Two disputes and that bank account is done. Stripe charges $15.00 per disputed payment and $4.00 per failed one, non-refundable regardless of outcome, and refunds are full-only.
None of that is a choice you make. It's the shape of the rail, and your data model either accounts for it or gets caught out by it.
The decisions you can't take back
Everything above happens to you. The next two you choose, usually without noticing, and you choose them once.
The mandate wording. Before you can debit a bank account the customer authorizes it, and Stripe stores that authorization: the accepted text, the acceptance date, the IP address. It's a record of what one person agreed to on one day, and you can't retroactively broaden it.
If you build a custom form, Stripe publishes recommended mandate text. If you use Checkout or the Payment Element, "Stripe displays the required mandate language and records acceptance automatically." The Hosted Invoice Page renders the mandate for you too.
Here's where it bites. If you ever want to become a platform paying out to other people, Stripe requires this:
"When collecting a bank account that you intend to clone to connected accounts, you must communicate to the customer that their authorization extends to connected accounts on your platform."
Stripe's published recommended text contains no such clause, and the hosted surfaces render language you don't author. So if the side project grows into a platform, every customer who accepted the narrower mandate has to accept a new one, and re-consent for a bank debit isn't a click in an email, it's the whole collection flow again. That decision gets made the first time a customer clicks Authorize, whether you knew you were making it or not.
The on_behalf_of parameter is the same trap in one line of code, and this one the API enforces rather than merely advises. "If a mandate is authorized for a PaymentIntent or SetupIntent on_behalf_of a connected account, you can't use that mandate with a different connected account." Set it when you first collect and that mandate is bound to one connected account permanently. Don't set it.
The testing trap
This one is cleanly circular:
"Test clock advancement currently doesn't support collecting payments through bank debits (for example,
us_bank_accountpayment method types)."
The single thing a Subscription buys you over rolling your own scheduling is automatic cycling, and automatic cycling with a bank debit is the single thing you can't fast-forward test. You can advance the clock and watch Stripe generate the invoice, but the subscription stays active, because collection was never attempted during the advance.
The good news: test transactions settle instantly in a sandbox, so the payment lifecycle itself is fully exercisable for free. It's the calendar you can't simulate, not the payment.
Which pushes me toward keeping the schedule in my own code and using target_date, a per-payment instruction for when the debit should land: "at least three days in the future and no more than 15 days from the current date," cancellable up to three business days before. That absorbs the settlement lag without handing Stripe a recurrence, and it's testable by moving my own clock instead of Stripe's.
The actual point
None of this is hidden. It's all in Stripe's documentation, just scattered across a pricing page, a support answer, a caveat at the bottom of a test-clock guide, and a subsection about Connect, and none of them is the page you land on when you search for how to accept payments. The constraints surface after you're already committed, which is the worst time to learn that a mandate can't be broadened.
The short version, if you're at the start of this: assume T+4, assume undocumented volume limits, write the mandate you'd want in two years, and don't trust succeeded.
The templates over at rdooley.dev ship with Stripe subscriptions already wired up, which is the part of this that's genuinely solved. The rest is what you sign up for the day you decide cards are too expensive for what you're selling.