PCI DSS is a long document, and most of it is about protecting card data you hold. The useful realisation for a developer is that the cheapest way to satisfy a requirement about card data is to not have any — and that this is an architectural choice you make in a handful of places, not a compliance exercise you do afterwards.
What brings a system into scope
Three verbs: store, process, transmit. Any system that does one of them to cardholder data is in your cardholder data environment, along with anything connected to it that could affect its security.
The word that catches people is transmit. An endpoint that receives a card number and immediately forwards it to a processor — writing nothing, keeping nothing — is transmitting cardholder data. It is in scope. So is the load balancer in front of it, the logging pipeline behind it, and the error tracker that might capture a request body.
Hosted fields, and why they work
The way out is to arrange for the card number never to reach you. Hosted fields do this by serving the actual inputs from the processor: each one is an iframe on the processor origin, positioned inside your layout so it looks like part of your form.
const form = await payments.form({ payment_method_type: "card" });
// Your divs. The processor renders the real inputs inside them, from its
// own origin — so the same-origin policy, not a promise, is what stops your
// JavaScript from reading what the customer types.
form.createField("cardNumber").inject("#card-number");
form.createField("cardExpiry").inject("#card-expiry");
form.createField("cardCvv").inject("#card-cvv");
await form.build();
// What comes back is a reference and display metadata. There is no method
// here that returns the card number, by design.
const paymentMethod = await payments.createPaymentMethod({
type: "card",
billing_details: { name: "Ada Lovelace", address: { zip: "80021", country: "US" } },
});
// -> { id: "pm_…", card: { brand: "visa", last4: "1111", exp_month, exp_year } }The card goes from the iframe straight to the processor. Your server sees a pm_… identifier and four digits. There is no point in the flow at which a system you operate holds the number, which is why this arrangement reduces your obligation rather than merely satisfying it.
What that changes on paper
| Integration | Card data touches your servers? | Typical questionnaire |
|---|---|---|
| Processor-hosted fields or redirect | No | SAQ A — the shortest |
| Your own form posting to the processor from the browser | No, but you control the payment page | SAQ A-EP — substantially longer |
| Your server receives and forwards the card | Yes | SAQ D — the full questionnaire |
| Your server stores card numbers | Yes, and at rest | SAQ D, plus storage and encryption controls |
The gap between the first and third rows is the entire point of this article. It is not a difference of degree — it is the difference between a short self-assessment and an obligation that realistically needs dedicated effort to maintain. And it is decided by where a single form input is served from.
The accidents that pull you back in
Scope is not set once. It is re-decided every time someone adds a feature, and each of these is usually added for a sensible reason by someone who has not thought about scope.
- A phone-order endpoint. Support needs to take a card over the phone, so someone adds a form that posts the number to your API. That single endpoint moves your servers into the cardholder data environment. Use a processor-hosted virtual terminal instead.
- A generic request logger. Body logging on an endpoint that receives card data writes card numbers into your logs, which are replicated, shipped to a third party and retained for a year.
- An error tracker capturing request payloads. Same failure, worse reach: the payload now sits with a vendor outside your control.
- A migration script. Importing card data from a previous provider puts the script, the machine it runs on and the file it reads in scope. Ask both providers to run a processor-to-processor migration instead — the card data never lands with you.
- A CSV from a partner. Any file containing card numbers is in scope from the moment it arrives, including in the inbox it arrived in.
- A screenshot in a support ticket. Not a code path, but a real one, and your ticketing system is not designed to be a cardholder data environment.
What is still yours
Keeping card data out removes the largest part of the obligation, not the whole of it. What remains is worth knowing, because it is the part a good integration still has to get right.
- The page that loads the payment form. A compromised script on your checkout page can overlay fake inputs on top of the hosted ones. Content Security Policy and integrity attributes on third-party scripts are the defences.
- Your API keys. The secret key stays server-side and out of the client bundle. In a Next.js app that means never giving it a
NEXT_PUBLIC_prefix — that prefix inlines the value into the browser bundle at build time. - Access control. Who on your team can see payment records, issue refunds, or read the customer data attached to them.
- Your vendors. Anything in the path of the checkout page — a tag manager, an analytics snippet, a session recorder — is part of your exposure. Session recorders are the notable hazard, since capturing an iframe is exactly their job.
- The self-assessment itself. Shorter, but not absent, and it has to be completed.
GOOD QUESTIONS
Frequently asked
What actually puts my server in PCI scope?+
Storing, processing or transmitting cardholder data. A raw card number arriving at your endpoint counts as transmitting it, even if you forward it immediately and never write it down. Scope follows the data, not your intentions for it.
What is the difference between SAQ A and SAQ A-EP?+
Both are for merchants who do not handle card data directly, but they differ by how much of the payment page you control. SAQ A applies when the payment form is wholly served by a third party, such as hosted fields in an iframe or a redirect. SAQ A-EP applies when your own page controls elements of the payment form, which brings in many more requirements. Hosted fields are what keeps most integrations in the shorter questionnaire.
Does using a token mean I am out of scope entirely?+
It removes the largest part of your obligation but not all of it. You still have responsibilities around the page that loads the payment form, your keys, your access controls and your vendors. What it removes is the cardholder data environment — the part that requires storage controls, network segmentation and evidence of how card numbers are handled.
Is it safe to log the request body on my payment endpoint?+
Only if you are certain no card data can be in it, which with hosted fields it cannot. The danger is a generic request logger on a route that does receive card data, because logs are replicated, shipped to third parties and retained far longer than anything you would deliberately store. Log the token, never the payload.
Do I still need to worry about PCI if my processor handles everything?+
Yes, in the reduced form. You are still responsible for the integrity of the page that loads the payment form, for keeping your API keys secret, and for completing the appropriate self-assessment questionnaire. What a good integration buys you is a much shorter questionnaire, not an absent one.
What is the one change most likely to expand my scope by accident?+
Adding a server endpoint that accepts card details — usually written for a good reason, such as a phone order taken by a support agent or a migration from another provider. That single endpoint moves your servers into the cardholder data environment. Route those cases through a processor-hosted form instead.
