Notes on PCI-aware checkout flows

Most developers building a checkout don’t need to become PCI DSS experts. They do need to understand how their design choices change the compliance burden on the business that runs the shop. A few early decisions can mean the difference between a short self-assessment questionnaire and a long, expensive audit.

These are the notes I wish I’d had when I started working on payment integrations. They’re written for developers, not compliance officers. Always confirm the details with your payment provider or acquirer.

The cheapest scope is the one you never enter

PCI DSS applies to any system that stores, processes or transmits cardholder data, and to anything connected to those systems. The most effective thing you can do is make sure card numbers never touch your servers at all.

Modern payment providers make this straightforward. The main integration styles, roughly from least to most scope, are:

Integration How card data flows Typical burden
Redirect / hosted payment page Customer leaves your site and pays on the provider’s page Smallest (usually SAQ A)
Hosted fields / iframe Card fields are served by the provider inside your page Small (usually SAQ A)
JavaScript direct post Your page’s script sends card data straight to the provider Larger (usually SAQ A-EP)
Server-side API Card data passes through your server Full scope (SAQ D)

If you are choosing today, start with a hosted page or hosted fields. Only move down that table if there is a business reason that outweighs the cost.

Your checkout page is still in scope

Hosted fields keep card numbers off your servers, but the page that contains them is still a target. Card-skimming attacks work by injecting malicious JavaScript into the checkout page, often through a compromised third-party script, a plugin, or a tampered file on the server.

PCI DSS v4 puts much more attention on this. It expects merchants to know which scripts run on payment pages, to justify each one, and to detect unauthorised changes. Even where your questionnaire doesn’t list those requirements in full, the risk is the same. Practical steps:

  • Keep the checkout page lean. Analytics, chat widgets, A/B testing tools and marketing tags rarely need to run on the payment step.
  • Use a Content Security Policy that only allows the scripts and frames you expect.
  • Use Subresource Integrity on third-party scripts where the provider supports it.
  • Watch for file changes on the server, especially in templates and anything the checkout loads.
  • Be careful with plugins. On platforms like Zen Cart, WooCommerce or Magento, a third-party plugin that loads on every page also loads on your checkout.

Trust the webhook, not the redirect

After a customer pays, the provider usually sends them back to your site. It’s tempting to mark the order as paid when that return request arrives. Don’t.

The return redirect comes from the customer’s browser. It can be lost when a tab closes, replayed, or forged by anyone who knows the URL. The reliable signal is the server-to-server notification from the provider: a webhook, IPN or notification callback.

A solid pattern:

  1. Create the order in a pending state before sending the customer to pay.
  2. Treat the browser return as a hint. Show a “we’re confirming your payment” page if needed.
  3. Mark the order paid only when a verified webhook confirms it.
  4. For extra safety, confirm the payment status with the provider’s API before fulfilling.

Make webhook handlers boring and safe

Webhook handlers are small, but they carry a lot of responsibility.

  • Verify every request. Check the signature or shared secret your provider sends. Reject anything that doesn’t verify, and never trust an order ID or amount from an unverified payload.
  • Be idempotent. Providers retry, and you will receive the same event more than once. Record each event ID and make processing it a second time a no-op.
  • Expect events out of order. A refund notification can arrive before you’ve finished processing the payment. Base decisions on the current state, not the order events arrive in.
  • Respond quickly. Acknowledge the webhook, then do slow work such as emails or stock updates in a queue or background job.
  • Log what happened, but see the next section about what not to log.

Never trust the browser with money

Anything the browser sends can be changed. The order total, the currency, discount amounts and shipping cost should all be calculated on the server, from your own data, at the moment you create the payment.

Then check again on the way back. When the webhook confirms a payment, compare the amount and currency the provider received with the order you expected. A mismatch should stop fulfilment and alert someone.

Be careful what you log

Logging is where card data most often leaks into scope by accident. A debug line that dumps the full request, a verbose error handler, or a support tool that records form submissions can all capture card numbers without anyone noticing.

  • Never log raw request bodies on payment endpoints.
  • Mask anything that looks like a card number before it reaches a log or error tracker.
  • Treat provider tokens and customer IDs as sensitive, even though they aren’t card numbers.
  • Check what your error-tracking and session-replay tools capture on the checkout page.

Plan for strong customer authentication

In the UK and EU, most online card payments need strong customer authentication, usually through 3-D Secure. The flow has to handle a customer being sent away to their bank’s challenge screen and coming back, or not coming back.

Modern provider APIs such as Stripe’s Payment Intents handle most of this for you, but only if your integration follows their state model. Don’t assume a payment is either “succeeded” or “failed”. There are states in between, such as “requires action” and “processing”, and your order logic needs to handle each one.

A short checklist

  1. Card data never touches your application servers.
  2. The checkout page loads only the scripts it needs, protected by a CSP.
  3. Orders become paid through verified webhooks, never the browser redirect.
  4. Webhook handlers verify signatures, are idempotent and tolerate out-of-order events.
  5. Amounts are calculated on the server and checked again on confirmation.
  6. Nothing sensitive reaches logs, error trackers or session recordings.
  7. The payment flow handles authentication challenges and in-between states.

None of this is complicated on its own. The hard part is getting it right at the start, because retrofitting a safer design into a live checkout is much harder than building it that way.