Say you open the Stripe dashboard on a Tuesday morning and the home chart has a spike you didn't earn. Two hundred payment attempts overnight, almost all failed, a few succeeded for your $1 starter plan. The customer names look like someone fell on a keyboard. Your actual business does maybe 40 payments a month.
That's card testing. Someone has a list of stolen card numbers and is using your checkout to find out which ones still work. Your product doesn't matter to them. Your form is just a cheap way to ask a bank "is this card alive?"
With under 100 customers this hits harder than it does for a big store. Your normal traffic is so small that a few hundred junk attempts can make your whole account look risky. So set a few things before launch day, not after the first spike.
I wrote earlier about the signup side of this, fake accounts and junk signups. This one is only the Stripe part.
What card testing looks like in Stripe
Stripe's own guide to protecting yourself from card testing lists the symptoms:
- a spike in failed or blocked payments on the dashboard home page and the transactions list
- a spike of 402 errors in your developer logs, a lot of them with
generic_decline - small payments that succeeded, usually with nonsense names and emails
For a small SaaS I'd also look for:
- one Customer object with many different cards attached
- lots of new Customers created from the same IP in a short window
- a pile of SetupIntents with no subscription ever starting after them
That last one matters. Stripe says testers prefer card setup flows, because validating a card during setup usually doesn't show up on the cardholder's statement, so the real owner doesn't notice. A "start your free trial, card required" form is a card setup flow. So is "add a payment method" on a settings page that anyone can reach.
Why tiny SaaS checkouts are a nice target
Small products tend to have exactly what testers look for:
- Free trial with card up front. A SetupIntent or a $0 trial checkout checks the card and charges nothing. Perfect for them.
- $1 or very cheap plans. Testers like low amounts because cardholders are less likely to notice.
-
An open endpoint that creates PaymentIntents, SetupIntents or Checkout Sessions. If any visitor (or any script) can hit
/api/create-checkout-sessionwithout logging in, that's a card checker with your name on it. - Low volume. Your normal decline rate is built on a tiny base, so a burst moves it a lot.
Stripe also says testers can use your publishable key against your site, and that simple IP blocks alone usually aren't enough.
What Stripe already does before you touch anything
If you use Stripe Checkout or the latest Payment Element, Stripe runs card testing controls on its side: rate limiters, AI models, CAPTCHA triggers when it detects an attack. Payments it stops show up as Blocked by Stripe. A hand-rolled card form against the raw API gets less of that.
On plans: Stripe's Radar pricing page lists Radar Lite as included at no extra charge on standard payments pricing, with AI-based fraud prevention for card payments, card testing prevention and fraud alerts. Card testing prevention is in every tier. What you don't get on the free tier, or on Radar Standard, is custom rules with backtesting. Those are in Radar Plus and Radar Pro (if you've read older posts that say "Radar for Fraud Teams", that's the feature set they mean). As I write this, the US page shows Plus starting at $14 a month or pay as you go. Check your own country.
Two cheap things that help the default protection work better:
- Send Stripe more data. Stripe says the models do better when you pass IP address, email, name and billing address, and it recommends loading Stripe.js on every page, not only checkout.
- Turn on Radar for saved cards. Per Stripe's page on optimizing Radar risk factors, Radar by default does not scan when you set up a payment method for future use without a charge. If your free trial collects a card with a SetupIntent, go to Radar settings and enable "Use Radar on payment methods saved for future use". Otherwise your trial form is the one door Radar isn't watching.
While you're there, check the built-in CVC and postal code risk-score rules. Stripe says they also apply when attaching a card to a customer, so they help setup flows too.
Radar rules worth adding (if your plan has custom rules)
Rules use this shape: {action} if {condition}. The rules reference has the details. Three things from it worth knowing before you write anything:
- Velocity counts exclude the payment being evaluated. So
total_charges_per_ip_address_hourlyis 0 on the first attempt from an IP in the hour, 1 on the second. - "Hourly" isn't exactly 60 minutes. Stripe uses 5-minute buckets, so it can be up to 3,900 seconds.
- Rules only affect future payments, not ones already processed.
Stripe's Radar Rules 101 guide gives these two as card testing rules:
Block if :total_charges_per_ip_address_hourly: > 1
Block if :blocked_charges_per_ip_address_hourly: > 1
The second one is the "more aggressive" add-on. For a B2B SaaS where a whole office might share one IP, > 1 on the first rule can be too tight, so backtest it before turning it on.
Since the trial is usually the weak spot, also use attributes that count SetupIntents. In the supported attributes list, card_count_for_ip_address_transactions_hourly counts cards from "transactions (charges and SetupIntents)", while card_count_for_ip_address_hourly only counts charges. Some starting points (the thresholds are my guess for a small SaaS, not Stripe's numbers):
Block if :card_count_for_ip_address_transactions_hourly: > 3
Block if :card_count_for_customer_daily: > 2
Block if :declined_charges_per_ip_address_hourly: > 3
Request 3D Secure if :is_setup_intent: and :risk_level: != 'normal'
Plain version: one IP trying four-plus cards in an hour, or one customer with three-plus cards in a day, is not a real customer. Someone who typos their card twice still gets through.
So these don't bite you:
- The card count attributes are bounded at 25, so don't write rules that need numbers above that.
- The rule editor tests a rule against past payments, and a traffic allocation of 0% runs it in shadow mode, reporting what it would have done. With thin history, a week in shadow mode is a decent sanity check.
- Be careful with
:cvc_check:in your own custom rules. It's a post-authorization attribute, and Stripe notes the cardholder may briefly see an authorization even if the charge ends up blocked. The built-in risk-score CVC rule is the easier option.
Fix your own endpoint, because Radar only sees what reaches Stripe
Radar can't stop your server from creating thousands of Checkout Sessions or Customers for a script. That part is on you.
- Require login (and ideally a verified email) before you create anything in Stripe. Stripe's guide says the easier it is to reach your payment form, the easier card testing gets. Don't create a Stripe Customer at signup "just in case". Create it when a verified user actually goes to pay or start a trial.
- CAPTCHA on the endpoint that creates the PaymentIntent, SetupIntent or Checkout Session, checked on the server. Stripe's troubleshooting list for a CAPTCHA that isn't working starts with: require it on every request that leads to a card validation or payment, and check it server side.
- Rate limit that endpoint per IP and per account. Stripe's own example is limiting how many new customers one IP can create in a day. Their combined idea is nice too: let the first attempt from an IP through with no friction, then require a CAPTCHA for that IP for the next few hours.
- Limit cards per account. Real customers add one card, maybe two.
-
Think about a trial without a card. If card-required trials are not core to your model, Checkout supports free trials without collecting payment details using
payment_method_collection=if_required, withtrial_settings.end_behavior.missing_payment_methoddeciding whether to cancel or pause when the trial ends. No card form at signup, nothing to test. It's a conversion tradeoff, your call. - Keep your secret key secret. If it leaks, testers can create payments and set up cards directly with it, and none of your front-end defenses matter.
If it's happening right now
Stripe's active card testing checklist is short: identify it, refund the fraudulent payments, add mitigations or move to a recommended integration, then keep monitoring. In practice for a small SaaS:
- Close the door first. Put the checkout or trial endpoint behind login, turn on the CAPTCHA check, or temporarily switch off card-required trials. Ugly is fine for a day.
- Refund the junk payments that succeeded. Stripe recommends refunding suspicious payments that got through, so they don't become early fraud warnings or disputes. If you use manual capture, cancel the uncaptured ones instead of capturing them.
- Don't retry the fake cards. Stripe calls this out: if your dunning keeps retrying cards attached to fraudulent customers, you're repeating the attack from your side. Cancel the fake subscriptions and clean up those customers.
- Add the velocity rules above if you have custom rules, in shadow first if you can afford to wait, straight to block if you can't.
- Contact Stripe support and tell them it's card testing. They can see things on their side you can't.
- Check the 402 graph for a few days to make sure it actually stopped and didn't just slow down.
Why it's worth an hour before launch
Stripe's guide lists the consequences, and none are about the few dollars of test charges:
- Disputes and early fraud warnings from the payments that went through. At under 100 payments a month, one dispute is already a 1% dispute rate, so a handful of them is a real problem.
- Higher decline rates for your real customers, even after the attack stops, because issuers start seeing your business as riskier.
- Extra fees, like dispute fees, and authorization fees if you're on a custom pricing plan.
- Card monitoring programs if the fraud warnings and disputes pile up.
- Junk data. Fake trials look like growth.
And there's the account itself. A small account that suddenly looks risky is how reviews and payout holds start. There's a longer piece on keeping your indie SaaS from getting locked out of Stripe and other critical accounts worth reading before that day.
Pre-launch checklist
- [ ] Checkout or Payment Element, not a hand-rolled card form, if you can
- [ ] Stripe.js loaded on every page, and IP, email, name, billing address passed to Stripe
- [ ] "Use Radar on payment methods saved for future use" turned on if trials collect a card
- [ ] Built-in CVC / postal code risk-score rules reviewed
- [ ] No Stripe Customer, PaymentIntent, SetupIntent or Checkout Session created before login + email verification
- [ ] Server-side CAPTCHA and per-IP / per-account rate limit on that endpoint
- [ ] Max cards per account
- [ ] If you're on Radar Plus or Pro: velocity rules in shadow mode, then block
- [ ] A saved filter or alert for failed payments and 402s, so you see a spike the same day
- [ ] Secret key only on the server, never in a repo or front-end bundle
None of this is security or legal advice, just the Stripe settings I'd check before putting a checkout link in front of a launch crowd.
One question for people who already run a card-required free trial on Stripe: did you turn on Radar for SetupIntents, and if so, did it block real trial signups or only the junk?
Top comments (6)
The SetupIntent point is the one I'd underline. On a "card required" trial, I'd only create the SetupIntent after the email is confirmed, and cap it at one open SetupIntent per account and a couple of card attachments per day. A tester working through a list needs hundreds of attempts, so making each attempt cost a confirmed signup kills the economics fast, and real users never notice.
tying the SetupIntent to a confirmed email is a really nice move, a real user loses a few seconds and a tester has to burn an inbox per attempt. the one-open-SetupIntent cap is smart too. do you limit by account only, or also by ip or device? confirmed emails can get pretty cheap for someone running a list.
Account alone isn't enough, you're right. I'd count attempts per account, per IP and per card fingerprint (Stripe puts one on every card PaymentMethod), and treat a fingerprint that already showed up on another account as the strongest signal. IP is worth a soft limit but rotates cheaply, so I wouldn't hard block on it. Cheap confirmed emails tend to cluster on a few domains or on plus and dot variants of one inbox, so normalising the address before the check catches a fair share of them too.
The detail most teams miss sits in the velocity section: hourly velocity excludes the payment being evaluated, so the first attempt from an IP is 0, not 1. A rule like block if total_charges_per_ip_address_hourly > 1 only bites on the third attempt, which is exactly why the paired blocked-charges rule earns its place. The 5-minute bucket note is the kind of thing that makes a rule behave differently in testing than in production. The strongest section is the one about Radar only seeing what reaches Stripe: a script hammering your endpoint that never gets authorized is invisible to every Radar model, so the login-plus-CAPTCHA gate on session creation is the actual defense and Radar is the safety net behind it. And not retrying fraudulent cards through dunning is a subtle catch; your retry engine would keep re-asking the attacker's card to pay, which is the attack continuing on your infrastructure.
the 0-not-1 thing on hourly velocity is exactly the kind of detail that looks fine in testing and then quietly lets the first two attempts through in prod. and yeah, radar is the net behind the gate, not the gate itself. do you put the captcha on every checkout session or only after a failed attempt from the same ip?
Only after a failed attempt from the same IP. Card testing fails fast: the declines cluster on the first handful of attempts, so the first failure from an IP is your cheapest tripwire, and putting the captcha on every session taxes every real customer to stop a threat that hasn't shown itself yet. Two honest caveats: serious testers rotate IPs, so IP-scoped captcha alone buys you an hour at best, and the thing that actually holds the line is the velocity gate keying on harder-to-fake signals (card count, BIN spread, email domain) with the captcha as the visible hand-off once those fire. Practically: no captcha, first decline from that IP trips a soft challenge, second one blocks the session and you never see the rest of the run.