Pulse
7 7IT Solutions
Payments

PCI Without the Pain: Taking Card Payments the Safe Way

Lior Aharonov Lior Aharonov 7 min read Updated 2026-06-22

PCI compliance sounds like a wall of audits and paperwork, and it can be, if you design your checkout the wrong way. The good news is that for most businesses the entire burden comes down to a single decision, and if you make it correctly, PCI shrinks from a project into a short questionnaire. Make it wrong, and you take on the full weight of protecting card data yourself, which is expensive, risky, and almost never necessary.

This guide is the practical version: how to take card payments safely while keeping your compliance scope as small as it can be. It is not legal advice, but it is the engineering that makes the legal part easy. The examples assume Stripe, and the same ideas apply to any modern processor.

The one rule that changes everything

Here it is: never let a raw card number touch your servers. PCI scope is driven almost entirely by whether card data passes through systems you control. If it does, you are responsible for securing every one of them. If it never does, your obligations collapse to the simplest level, a short self-assessment, because there is nothing sensitive in your environment to protect. Every recommendation below is in service of that one rule.

How to keep card data off your servers

Modern processors are built so you never have to handle the card number. Instead of your own <input> for the card, you embed a field that the processor serves inside an iframe, Stripe Elements or its hosted checkout, so the digits are typed directly into the processor's domain, not yours. Your page never sees them. What you get back is a token, a harmless reference that stands in for the card.

// the card field is an iframe served by Stripe; your code never sees the number
const { error, paymentMethod } = await stripe.createPaymentMethod({ elements });
// you receive a token reference, not the card data
await fetch("/api/pay", {
  method: "POST",
  body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
});

Tokenization: charge the token, store nothing sensitive

On the server you work entirely in tokens. You charge the token, and to bill a returning customer you save the processor's customer reference, not the card. The actual card details live with the processor, in their vault, under their compliance, and your database holds only ids that are useless to a thief.

// the server only ever handles tokens, never the card number
await stripe.paymentIntents.create({
  amount, currency: "usd",
  customer, payment_method: paymentMethodId, confirm: true,
});
// store Stripe ids only; never store the card number or CVV

What you must never do

A few actions drag card data back into your scope and undo everything, so treat them as hard rules. Never build your own card input that posts the number to your backend. Never store the full card number, and never, under any circumstances, store the CVV, even encrypted. Never log card data, and never accept it over email or phone into a system that keeps it. The moment a real card number lands in your logs or database, you own the full PCI burden, so the discipline is to make sure it simply never arrives.

Lock down the checkout page

Keeping the number off your server is most of the battle, but the page around the payment field still matters, because attackers skim checkout pages by injecting a malicious script that reads the page (the Magecart pattern). Serve everything over HTTPS, set a strict Content-Security-Policy so only scripts you trust can run, use subresource integrity on third-party scripts, and keep the number of external scripts on your checkout to the bare minimum. A clean, locked-down checkout page is far harder to skim.

Hosted checkout versus embedded fields

You have a spectrum, and all the good options keep your scope minimal. A fully hosted checkout, where you redirect the customer to the processor's page, is the least effort and the least scope, ideal when you do not need a custom look. Embedded fields like Stripe Elements give you a checkout that lives inside your own design while the sensitive input is still served by the processor, which keeps you at the same minimal scope with more control. The one option to avoid is a custom card form that handles the number yourself, which buys you a little styling freedom at the cost of the entire PCI burden. It is almost never worth it.

Compliance is ongoing, not a certificate

Reducing scope does not mean ignoring security. Keep doing the basics: complete the self-assessment honestly, keep your dependencies and servers patched, limit who has access to payment systems, and re-check that no card data has crept into a log or an export. PCI is a posture you maintain, not a box you tick once, but with card data kept entirely off your systems, maintaining it is genuinely light.

A PCI scope-reduction checklist

  • Never let a raw card number reach your servers, this is the rule that sets your scope.
  • Use processor-served fields (hosted checkout or embedded Elements), not your own card input.
  • Work in tokens, store the processor's ids, never the card number and never the CVV.
  • Never log card data or accept it over email into a system that retains it.
  • Serve checkout over HTTPS with a strict CSP and minimal third-party scripts.
  • Prefer hosted or embedded fields over a custom card form.
  • Keep up the self-assessment, patching, and access limits over time.

FAQ

How do I reduce my PCI compliance scope?

Keep raw card data off every system you control. PCI scope is driven by whether card numbers pass through your environment, so if you use processor-served fields, hosted checkout or embedded Elements, the digits go straight to the processor and never touch your servers, which drops you to the simplest self-assessment level. Work entirely in tokens on your side. The less card data in your systems, the smaller your compliance burden, ideally nothing to protect at all.

Can I build my own custom card form?

You can, but you almost never should. A custom form that handles the card number itself pulls that data into your environment and saddles you with the full PCI burden, securing, auditing, and proving compliance across every system it touches. Embedded fields like Stripe Elements give you a checkout inside your own design while the processor still serves the sensitive input, so you get the look you want without the compliance weight. The styling freedom is not worth the cost.

Is it ever okay to store a card number or CVV?

Never store the CVV, full stop, even encrypted, it is prohibited after authorization. And you should not store the full card number either. To charge a returning customer, save the processor's customer or payment-method token instead, which lets you bill them again while the real card details stay in the processor's vault. Your database should contain only references that are worthless to anyone who steals them.

What is Magecart and how do I protect against it?

Magecart is the technique of skimming card details by injecting a malicious script into a checkout page that reads what the user enters or manipulates the page. Even when the card field is a processor iframe, a compromised page around it is a risk, so protect against it with HTTPS everywhere, a strict Content-Security-Policy that only allows trusted scripts, subresource integrity on third-party code, and as few external scripts on checkout as possible. A locked-down page is much harder to skim.

Does using Stripe make me automatically PCI compliant?

It makes compliance easy, not automatic. Using processor-served fields keeps card data off your servers and reduces you to a short self-assessment, but you still have to complete that assessment, keep systems patched, limit access, and ensure no card data leaks into logs or exports. Stripe handles the vault and the heavy lifting, and your job is to keep your side clean and not undo that by mishandling data. It is light, but it is not nothing.

If you are building a checkout and want to take cards safely without signing up for an audit, tell me how you want to charge and I will map out the lowest-scope way to do it.

Want a hand applying this?

Tell me where your business is stuck and I will give you a straight, useful read, no pitch.

Go deeper

Payments

Embedding Stripe in a Headless WooCommerce and Next.js Store: What Actually Matters

A practical, field-tested guide to wiring Stripe into a headless WooCommerce and Next.js storefront, covering keys and authorization, the UI layer, tokens and PaymentMethods, the backend PaymentIntent, authorize-then-capture, and the webhook details that quietly break in production.

Read →
Payments

Revolut for WooCommerce: Why I Built My Own Integration

The off-the-shelf Revolut gateway got a WooCommerce store most of the way there, but the wallets were unreliable, the checkout jumped, and the card fields were so bare they cost customer trust. Here is why I rebuilt the integration around Revolut's secure card field, and how owning the UI fixed it without touching PCI scope.

Read →
Payments

Revolut Merchant API on Vercel: Wallets, 3D Secure, and the Sync vs Async Trap

A field-tested guide to the Revolut Merchant API on Vercel, covering the order-first flow, Apple Pay and Google Pay, how 3D Secure is handled, the sync versus async reality that catches teams out, and the webhook signature details that must be exactly right on a serverless function.

Read →
Shopify

The Customer Portal Shopify Doesn't Give You (Reorders, Invoices, Approvals)

Fast reorders, invoice and PO access, saved carts, buyer approvals, and rich order history are where Shopify's native accounts stop short. Here is what a real customer portal looks like, often headless, and how we build it in phases you can trust.

Read →
Custom Software

Smart Scraping Bots That Fake an iPhone: How Cloudflare and Vercel Fight Back

Modern scrapers spoof a real iPhone user agent while arriving from a datacenter ASN halfway across the world. Here is how that trick works, why blocking by user agent fails, and the Cloudflare and Vercel bot-protection features that actually stop them without turning away real customers.

Read →
eCommerce

When You Need a Custom WooCommerce Plugin (and When You Don't)

How to tell whether your WooCommerce store needs a custom plugin or whether an existing one will do, and how custom code can cut plugin bloat.

Read →