Payment security: what you must never store, and why webhooks are signed
Most online payment incidents don't come from a sophisticated attack, but from two design decisions: storing card data you never needed, and trusting whatever the customer's browser says.
Accepting cards means complying with the PCI DSS standard. The good news is that most of that compliance can be delegated: if your server never sees a card number, your provider carries almost all of the work. The bad news is that there are two easy ways to ruin it.
Data you cannot store, not even encrypted
The standard separates data that may be stored with protection from data that may never be stored once the payment is authorised:
- The three- or four-digit verification code on the back or front.
- The full contents of the magnetic stripe or chip.
- The PIN or its encrypted block.
Encrypting them does not fix it: the prohibition is on storage, not on format. The card number itself may be stored under strict conditions, but the right question is almost always a different one: what do you want it for? If it is for recurring charges, what you need is a token, not the number.
SAQ A: the sensible target for almost everyone
The lightest self-assessment questionnaire, known as SAQ A, is designed for merchants who fully outsource the capture of card data: the form is served by the payment provider inside an iframe or via redirect, and your page never touches the number.
The practical difference is enormous: instead of protecting, segmenting, and auditing your own infrastructure, you answer a short questionnaire. The trade-off is that the form is no longer yours, with the design limits that implies, and that any shortcut — capturing the number "just for a moment" to forward it — takes you out of that regime without warning.
Tokens: two different things with the same name
- A provider token replaces the card number inside your system. It lets you charge again without asking for the card again, but it only works with that provider.
- A network token, the thing behind Apple Pay and Google Pay, is issued by the card network itself and comes with a cryptogram that changes on every transaction. If someone intercepts it, it is useless to them.
In both cases the idea is the same: the sensitive value stops circulating. It is the same logic that puts an access grant on your server rather than on the phone, as explained in in-app payments.
Why webhooks are signed
When a payment completes, the customer's browser returns to your site on a return URL. That URL is not a trustworthy source: the customer controls it, and anyone can visit it. If your system marks an order as paid because a browser landed on `/thank-you`, you have a security problem, not an implementation detail.
The trustworthy source is the webhook: a signed server-to-server call from your payment provider. The usual signature is an HMAC-SHA256 of the exact message body with a shared secret, sent in a header. Verifying it properly has four rules:
- Sign and verify over the raw body, before parsing the JSON. Reordering keys changes the signature.
- Compare in constant time, not with an ordinary string comparison.
- Check the message's age if it carries a timestamp, so a captured request is not still valid months later.
- Be idempotent: the same event can arrive several times. Processing it twice must not grant access twice or issue two invoices.
And one more rule: if the signature does not validate, the correct response is an error, not "I'll process it anyway just in case".
The server decides the amount
The final classic: accepting an amount that comes from the client. A form field, a URL parameter, or a hidden input are all editable. The amount to charge must always come from the catalogue on the server; what the client sends is, at most, a product reference. The three phases of a payment — and where each check belongs — are described in what actually happens inside a card payment.
Logs: less is more
Payment systems produce a lot of logs, and that is where data leaks by accident: full signature headers, untrimmed webhook bodies, emails and addresses in plain text. Log identifiers and states, not contents, and define how long they are kept. A log nobody reads and nobody deletes is just attack surface.
In short
Payment security is won on boring decisions: don't store what you don't need, don't believe the browser, verify signatures properly, and be idempotent. None of it is visible to the customer, and all of it shows the day something goes wrong. You can read the rest of the articles on security and compliance, or see how payment states look in the demo.
Building subscriptions?
Check the pricing and try the dashboard with sample data before integrating anything.