Taking payments in WordPress without becoming a payments company
Property submissions need checkout, which means Stripe, PayPal, SCA and webhooks. The goal is to touch as little of that as possible while still being correct.
Real estate sites often charge for listings: a fee per property, a featured upgrade, a membership. So our themes need checkout, which means I maintain a Stripe integration and a PayPal integration, and think about payments more than I ever intended to.
The guiding principle has stayed the same throughout: handle as little of this as possible.
Never touch the card
The single most valuable architectural decision is that card details never reach the site. Stripe Elements and the PayPal SDK collect them in contexts we do not control and cannot leak. What comes back is a token or an order ID.
This is not only about compliance scope, though that alone justifies it. It is about what happens when a customer's WordPress install is compromised through some unrelated plugin, and on a long enough timeline, some of them will be. If card data was never there, that incident does not become a catastrophe.
SCA made the naive flow illegal
Strong Customer Authentication ended the simple model where you charge a card and get a yes or a no. Now a payment may need an extra authentication step, mid-flow, and your code has to handle "not finished yet" as a first-class state rather than an error.
Practically, that meant moving to Stripe's Payment Intents and accepting that a payment is a small state machine, not a function call. It also meant our UI had to stop assuming that a submitted form is a completed purchase.
Any payment flow that treats "pending" as an edge case will eventually charge someone twice or publish something for free.
The webhook is the source of truth
The browser is not a reliable narrator. Users close the tab on the redirect, lose signal, or double-click. If your listing goes live because a JavaScript callback fired, you will eventually have paid listings that are not live and live listings that were never paid for.
So the rule is: the browser gives you optimism, the webhook gives you truth. Fulfilment happens when the provider tells the server it happened, and that handler has to be idempotent, because providers retry and duplicate deliveries are normal.
Boring things that matter
- Log every state transition with the provider's own identifiers. When a customer says "I was charged twice," you need to answer in minutes.
- Test the failure paths, not the happy one. Declined cards, abandoned authentication, webhook arriving before the redirect.
- Say what currency and what is being bought, plainly, next to the button. A surprising number of disputes are really confusion.
None of this is clever. Payments is a domain where cleverness has an unusually high cost and an unusually low ceiling; the best possible outcome is that nobody ever thinks about it.