The rule that decides everything else
Do not let card numbers touch your servers.
Every other decision about payments follows from that one. Use a processor (Stripe, Razorpay, Adyen, PayPal, Paddle, whichever fits your country) and let their hosted checkout or their JavaScript element collect the card. The card details go from the user’s browser to the processor. Your server receives a token, and the token is useless to anyone who steals it.
The practical reason is scope. Card industry rules impose a large compliance burden on anybody whose systems handle raw card data, and a very small one on merchants who fully outsource collection. Building your own card form does not just add security risk; it adds an audit obligation that a small team cannot carry. There is no version of this where writing the form yourself is the sensible choice.
What you may store: the token, the last four digits, the card brand, the expiry month for display, the processor’s customer ID. What you must never store: the full number, the CVV (which may not be retained after authorisation under any circumstances) or the magnetic stripe data.
The webhook is the part that goes wrong
Payment flows are asynchronous. The user is redirected back to your site before the payment is confirmed, and the confirmation arrives separately as a webhook. Three consequences that cause most payment bugs:
1. Never grant access on the redirect alone. The return URL is under the user’s control. Grant on the webhook, or on a server-side check of the payment’s status.
2. Verify the signature. Every processor signs its webhooks with a shared secret. An unverified webhook endpoint is an unauthenticated “give this user a subscription” API, and it is on the public internet.
event = stripe.Webhook.construct_event(
payload=request.data,
sig_header=request.headers["Stripe-Signature"],
secret=os.environ["STRIPE_WEBHOOK_SECRET"]) # raises on tamperingUse the raw request body for verification. A framework that parses and re-serialises the JSON changes the bytes and every signature fails: a very common half-day of confusion.
3. Expect duplicates and reordering. Processors retry until they get a 2xx, so the same event will arrive twice. Store the event ID and ignore ones you have already processed. Events can also arrive out of order, so make each handler decide from the current state rather than assuming the previous event has been seen.
Respond quickly: acknowledge, then do the work in a queue. A slow webhook endpoint gets retried, which produces the duplicates you are now handling.
Idempotency, in the other direction
When you call the processor to charge, send an idempotency key: a value you generate per logical operation. If the network drops after the charge is made but before you get the response, retrying with the same key returns the original charge rather than making a second one.
Without it, the retry lesson’s advice (do not retry non-idempotent operations) means you must not retry a charge at all, and you are left with a payment in an unknown state and a customer who has been debited.
The states nobody designs for
Build the schema so these are representable, because they will happen: pending (initiated, no outcome yet), failed, succeeded, refunded, partially refunded, disputed, and expired. A boolean paid column will not survive contact with a chargeback.
Record the processor’s own IDs for every object, and reconcile: a nightly job comparing your records against the processor’s list of transactions for the day, reporting anything that exists on one side only. This catches missed webhooks, duplicated grants and, occasionally, a genuine bug that has been quietly giving away subscriptions.
Testing, and one uncomfortable truth
Every processor has test cards for success, decline, insufficient funds and the authentication challenge. Use them, and use the CLI tools that replay webhooks to your local machine so you can develop the asynchronous half properly.
The uncomfortable truth is that the flows you cannot fully test are the ones that hurt: a dispute months later, a refund crossing a subscription renewal, a currency conversion, a failed renewal on an expired card. Write down what your system should do in each case before you launch, even if you cannot rehearse them.
Regional reality, stated plainly
Payment rules differ by country in ways that change your design and cannot be papered over: strong customer authentication requirements in Europe, mandated tokenisation and specific recurring-payment rules in India, sales tax and VAT registration thresholds that vary by jurisdiction. A merchant of record (a service that sells on your behalf and handles tax) costs a higher percentage and removes a genuine administrative burden for a small team selling internationally.
This is a real trade-off with money on both sides, and the right answer depends on where you and your customers are. It is also the point where the honest advice is to read your processor’s country documentation and, past a certain revenue, ask an accountant. Nothing in this lesson is legal or tax advice.
Every lesson here is open. Your progress is recorded once you pass the Agents and Automation exam.
Check your understanding
An app grants a subscription when the user is redirected to /success?session_id=... after checkout. What is the flaw?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Tri, your AI tutor
