A site selling £15 a month that only charged once.
There is no confidentiality constraint on this one, which is why it can be this specific — the platform is Instrumental Teachers Hub. Everything below happened on a live site with live Stripe keys and real cards.
The page said subscription. The system did not.
The site advertised membership at £15 a month, cancel anytime. Behind that was a plain WooCommerce product: it took one payment, once, and never asked for another. There was no subscriptions engine anywhere in the stack. A member who joined in January was still a member in June, and Stripe had never been told to charge them again.
There was also no cancellation control. "Cancel anytime" was a sentence on a sales page rather than a thing a member could do, because there was no subscription object to cancel.
The access rule was the part that mattered most. Membership was granted by an Automator recipe whose trigger was a user landing on a URL — not a completed payment, not a webhook, not an order status. Reaching the page was the qualifying event.
Nothing revoked it either. There was no path in the system that took membership away: not a refund, not a failed payment, not a cancellation. Access was a one-way door with the handle on the outside.
Three things that closed off the easy answers.
The site was live
Live site, live Stripe keys, real members already on it. Nothing could be taken down to be rebuilt, and no change could put an existing member into a broken state on the way past.
UK cards mean SCA
Under PSD2, Strong Customer Authentication is mandatory for UK cards. Any checkout that cannot present a 3D Secure challenge does not fail gracefully — it declines, and the customer is simply unable to buy.
Pre-revenue
The platform had not yet earned anything. A recurring licence fee to collect recurring revenue is a reasonable trade later and an awkward one before the first renewal has ever cleared.
Three ways to do it. One of them declines cards.
WooCommerce Subscriptions
The official extension, $279 a year. It works, it is well supported, and it is the answer we would give a client already running significant revenue through Woo.
Here it meant paying an annual licence, before any revenue existed, for a subscriptions layer sitting on top of a payment processor that already does subscriptions natively.
A free subscriptions plugin
The obvious way to avoid the licence, and the one most forum threads recommend. We read the documentation before installing anything.
The leading free option states plainly in its own docs that it does not support 3D Secure. For UK cards under PSD2 that is not a limitation to work around — it is a checkout that declines. Free would have cost us every customer.
Stripe Billing, direct
Stripe already handles recurring billing, SCA, retries, dunning and cancellation. The subscription is the processor's own object rather than a WordPress approximation of one kept in sync with it.
What was left to build was the part that is genuinely ours: turning a paid subscription into access, and the absence of one into no access.
Stripe owns the money. WordPress owns the door.
A real subscription in Stripe
Product and recurring price configured in Stripe, sold through a Stripe payment link. Checkout, 3D Secure, renewals, failed-payment retries and cancellation are all handled by the processor, which is the one system in the stack that has to be right about money anyway.
A custom WordPress plugin, listening to webhooks
A small plugin written for this job receives Stripe's webhooks and verifies the signature on every one before acting. Access is now granted by a payment event and revoked by a cancellation or a lapse — the event that changes the money is the event that changes the access.
A dedicated role, and group membership kept in step
Members get a role of their own rather than being folded into an existing one, so what a subscription grants is legible and reversible. Community group membership is synced to the same signal, so cancelling does not leave someone inside the members' area with the role removed.
Activation that does not depend on email
A new member is activated in the flow they are already in, rather than being sent to their inbox to find a link. Email arrives late, lands in spam, and is the step where people give up — so nothing in the path from paying to being inside depends on one being read.
Stripe's hosted portal for self-service
Cancelling, updating a card and reading invoices all happen in Stripe's own customer portal. "Cancel anytime" is now enforced by the company holding the card rather than promised by the site selling the membership.
No secret key on the site.
A custom billing portal inside WordPress would have been a nicer piece of product. It needs a Stripe secret key stored on the site to build it, and that key can create charges, issue refunds and read every customer record on the account.
This site carried known vulnerabilities in third-party plugins. Putting a live secret key on it would have meant that any one of those becoming exploitable escalated from a WordPress problem into a Stripe account problem.
So we did not store one. The site holds no credential capable of moving money. It receives signed webhooks and sends members to Stripe's hosted portal, which means the worst case for a site compromise stays bounded by what WordPress itself controls.
The trade is real and worth stating: members leave the site to manage billing, and the portal is styled by Stripe rather than by us. We took the plainer flow, because the alternative put the payment account inside the blast radius of a plugin update.
Two faults that would have hit the first real buyer.
Both were found by walking the whole path with a real card rather than by testing the pieces. Neither would have produced an error in a log.
The page builder was eating the activation
The activation step rendered its output through the page builder, which discarded it silently. No error, no warning, no entry anywhere — the page simply loaded without the part that mattered.
A member would have paid, been returned to a page that looked entirely normal, and not been activated. The only signal would have been a support email from someone who had already been charged.
Activation could hijack a logged-in session
Opening an activation link while already logged in as somebody else attached the new membership to the session that was already there, rather than to the person who had just paid.
On a shared machine, or for anyone signed in as an administrator while testing, that is the wrong account getting the membership — and the buyer getting nothing. Both were fixed before launch.
The page and the system now say the same thing.
- +Recurring billing works. The £15 a month advertised on the page is the £15 a month Stripe collects, on a schedule, with SCA handled.
- +Cancel anytime is honoured by Stripe, in Stripe's own portal, rather than being a claim the site cannot act on.
- +Access is granted by a verified payment event and revoked automatically when the subscription ends. Landing on a URL grants nothing.
- +No Stripe secret key is stored on the site, so a WordPress compromise cannot reach the payment account.
- +£0 a year in plugin licences. The recurring cost of collecting recurring revenue is Stripe's transaction fee and nothing else.
The wider platform this sits inside — membership, community, the companion apps and the AI marking feature — is covered in the Instrumental Teachers Hub case study.
Does your billing do what your pricing page says?
If you are not certain, that is the answer worth checking. Tell us what the page promises and what the system actually does, and we will tell you which of the two is lying. We reply within one working day. No calls required.