Do I Need to Be PCI Compliant to Take Payments in My App?
6 min read
Yes, PCI compliance applies to you the moment you take a card, and no, that does not mean paperwork and audits. If you let Stripe collect the card number and never touch it yourself, you land in the simplest tier of PCI (called SAQ A) and Stripe carries almost all of the weight. The one rule that keeps you there is blunt: a raw card number must never reach your server or your database.
Why this happens
PCI DSS is a card-industry standard, not a law, but Visa and Mastercard make your payment provider enforce it, so in practice it is mandatory. It scales with what you handle. Hand the card details straight to Stripe and your obligations are tiny. Type them into your own form and store or forward them, and you have just signed up for the heavyweight version meant for large processors.
The trap for a vibe-coded app is that an AI agent will build exactly the form
you describe. Ask for "a checkout page with card number, expiry and CVC fields"
and the agent produces plain <input> boxes that POST to your backend. It works
in the demo. It also means real card numbers are now flowing through your code,
which is the one thing PCI is designed to stop. The agent built what you asked
for, not the safe version you didn't know to ask for.
How to check
Look at how a card actually enters your app.
-
Open your checkout page and inspect the card field. If it is a normal
<input type="text">that you styled yourself, that is a red flag. Stripe's real card fields render inside an iframe hosted by Stripe, so the number never lives in your page's DOM. -
Search your codebase for where card data would land:
grep -ri "card_number\|cardnumber\|cvc\|cvv\|card.expiry" src/Any route that receives those values, or any table column that stores them, means card data is passing through you.
-
Check your network tab during a test payment. The card number should go to a
stripe.comorjs.stripe.comendpoint, never to your own/api/...route.
If cards only ever go to Stripe and you receive back a token or a completed session, you are in the safe tier. If they touch your server, you are not.
The fix
- Use Stripe Checkout or Payment Elements, not a hand-rolled form. Stripe Checkout redirects to a Stripe-hosted page; Payment Elements drops Stripe's own iframe fields into your page. Either way, you never see the number.
- Delete any custom card fields the agent built and remove any backend route that accepts card details. Confirm no database column stores a PAN (card number), CVC, or expiry.
- Create the charge from your server using only tokens. Your backend talks
to Stripe with your secret key and a
payment_intentorcheckout.session, never with raw card data. - Confirm payments server-side, not from a browser redirect. Treat an order as paid only when a verified Stripe webhook says so, which also closes the door on faked success. See Can a user fake a successful payment?
- Fill in Stripe's SAQ A self-assessment when prompted in the dashboard. It is a short questionnaire, not an audit, and using Checkout is what qualifies you for it.
The trap to avoid
The common wrong move is building your own pretty card form "just for the look" and then calling Stripe from the browser with the number. Styling the fields yourself, proxying the number through your API "to log it," or storing the last four digits from the raw PAN all pull you out of SAQ A into the heavyweight tiers. Do not build the field. Let Stripe's iframe be the field. It can be styled to match your brand without ever handing you the number.
Where this fits
Payments are the part of a beta where a quiet mistake gets expensive, and "the card form works" hides whether the card ever safely left the browser. The free Readiness Report checks your real project for card data touching your server and for the token-only pattern being in place, before your first paying user. If you would rather have the checkout rebuilt the safe way and verified end to end, that is what the Finishing Pass covers. Before you go live, also walk through how to test Stripe payments before launch.