EP Ecommerce Subscriptions
EP Ecommerce Subscriptions adds recurring billing to EP Ecommerce. Works with Stripe and PayPal as payment providers, handles the full subscription lifecycle from activation through renewal, cancellation, and expiration, and automatically grants and revokes membership access as payments succeed or fail.
Published by ElmsPark Studio.
Overview
Section titled “Overview”Features at a glance:
- Provider-agnostic subscriptions table with billing cycle tracking.
- Stripe integration creates Stripe Subscriptions (not one-off PaymentIntents) with proper Customer creation and lookup.
- Stripe webhook handling for
invoice.paid,invoice.payment_failed,customer.subscription.deleted,customer.subscription.updated. - PayPal integration with server-driven subscription create and webhook handlers for activation, cancellation, and renewal.
- Automatic membership grants on subscription activation.
- Automatic revocation on cancellation or expiration (per your cancellation-policy setting).
- Cancellation policy: end-of-period (access remains until paid-through date) or immediate (access revoked on cancel).
- Dunning and grace periods for failed payments: 0, 3, 7, or 14 days before access is revoked.
- UK/EU compliance: VAT display (inclusive or exclusive), cancellation rights notice, 14-day cooling-off period text.
- GDPR consent text on checkout (integrates with EP GDPR for consent logging).
- Newsletter opt-in on subscription checkout (integrates with EP Newsletter).
- EP Affiliate recurring commissions on every renewal.
- Auth-gated self-service via EP Passkeys.
The subscription lifecycle
Section titled “The subscription lifecycle”- Checkout. Customer picks a subscription product, enters card details, approves any GDPR consent or newsletter opt-in.
- Activation. Payment provider creates a subscription. The plugin stores a row with status = Active, billing cycle, and next renewal date.
- Access granted. A membership is created on the EP Ecommerce base (or equivalent), giving the customer access to gated content.
- Renewals. On every billing cycle, the provider charges. If successful, the plugin extends the membership paid-through date.
- Failed payments. If a renewal fails, the plugin enters dunning. The customer gets an email to update their payment method. During the grace period, access remains.
- Grace expires. If dunning doesn’t resolve within the grace period, access is revoked.
- Cancellation. Customer or admin cancels. Per your policy, access ends at the end of the current paid period (default) or immediately.
Requirements
Section titled “Requirements”- PageMotor 0.8.2b or later
- EP Ecommerce (base plugin)
- EP Suite base class
- EP Ecommerce Stripe (for Stripe subscription processing)
- EP Ecommerce PayPal (optional, for PayPal subscriptions)
Optional but commonly paired:
- EP Email for lifecycle emails (activation, renewal receipt, dunning, cancellation confirmation, expiration).
- EP GDPR for consent logging.
- EP Newsletter for opt-in at checkout.
- EP Passkeys for auth-gated self-service.
- EP Affiliate for recurring commissions.
Installation
Section titled “Installation”- Install EP Ecommerce, EP Ecommerce Products, and at least one payment extension (Stripe and/or PayPal).
ep-ecommerce-subscriptions.zipcomes with an EP Suite licence — ElmsPark supplies it directly (see EP Suite plugins); after install it updates through your site’s Updates screen.- Upload via Plugins → Manage Plugins. Activate.
Creating a subscription product
Section titled “Creating a subscription product”- Open Plugin Settings → EP Ecommerce → Products.
- Click Add Product and pick Subscription as the type.
- Fill in name, price, billing interval (monthly / yearly / custom).
- Set the membership level this subscription grants.
- Save.
On the product’s checkout page, EP Ecommerce Products renders the checkout with the price, billing cycle, and cancellation rights notice. The customer can pay with any active provider.
Cancellation policies
Section titled “Cancellation policies”Two options, set globally in settings:
- End-of-period. Customer keeps access until the paid-through date, then loses access. Default, and what most users expect.
- Immediate. Access ends the moment cancel is clicked. Customer does not get a refund for unused time.
Set this based on your business model and pick the one your terms of service describe.
Dunning
Section titled “Dunning”When a renewal payment fails:
- Provider fires the failure webhook (
invoice.payment_failedfor Stripe, similar for PayPal). - Plugin marks the subscription as past due and the grace period begins.
- EP Email sends the dunning email with a link to the self-service portal to update payment.
- Provider retries according to its own dunning schedule (Stripe and PayPal both auto-retry).
- If payment succeeds, subscription returns to Active, grace period ends.
- If grace period expires without a successful retry, subscription moves to Unpaid, access is revoked.
Grace period is configurable: 0 (no grace, immediate revoke), 3, 7, or 14 days.
Self-service shortcodes
Section titled “Self-service shortcodes”Place on the customer’s account page:
| Shortcode | Purpose |
|---|---|
[ep-my-subscriptions] | Lists the current user’s active subscriptions with cancel buttons and renewal dates. |
[ep-my-orders] | Full order history, including subscription payments. |
[ep-my-downloads] | Links to digital downloads the user has purchased. |
For auth, install EP Passkeys and gate the page. The shortcodes identify the current user from the authenticated session.
UK/EU compliance
Section titled “UK/EU compliance”The plugin adds compliance boilerplate to checkout:
- VAT display inclusive or exclusive based on settings and customer country.
- Cancellation rights notice — the statutory text about the 14-day cooling-off period for digital services (with an opt-out if the customer consents to immediate delivery).
- Consent-to-immediate-delivery checkbox waiving the cooling-off period, required for digital products delivered instantly.
These comply with the UK Consumer Rights Act and EU Consumer Rights Directive defaults. Adjust language per your own terms if your product falls under a different regime.
Integrations in detail
Section titled “Integrations in detail”- EP Affiliate fires a commission event on every renewal, not just the first sale. If configured, affiliates earn recurring revenue.
- EP GDPR receives consent data on subscription signup, stored in EP GDPR’s consent log.
- EP Newsletter can add an opt-in checkbox to the checkout; customers who tick are auto-added to the selected list.
- EP Passkeys gates the self-service shortcodes. Without Passkeys (or equivalent auth), the shortcodes show a login prompt.
Troubleshooting
Section titled “Troubleshooting”“Subscription activated but access wasn’t granted”
Section titled ““Subscription activated but access wasn’t granted””Check the membership-level mapping on the product. If the product doesn’t map to a membership level, the plugin doesn’t know what to grant.
“Cancelled subscriptions still have access”
Section titled ““Cancelled subscriptions still have access””Cancellation policy is probably End-of-period. Access stays until paid-through. If you want Immediate, change the policy setting.
“Failed payments aren’t triggering dunning emails”
Section titled ““Failed payments aren’t triggering dunning emails””Check EP Email is installed, configured, and queuing emails correctly. Also check the webhook is being received — if the provider can’t notify you of the failure, the plugin never knows.
“Customer paid via PayPal and never activated”
Section titled ““Customer paid via PayPal and never activated””Check PayPal webhook is receiving events. BILLING.SUBSCRIPTION.ACTIVATED is the one that flips the subscription to Active. Event history is in the PayPal Developer Dashboard.
“I want to extend a subscription by N days manually”
Section titled ““I want to extend a subscription by N days manually””Edit the subscription row’s paid-through date via SQL, or through EP Assistant with a prompt like “extend subscription ID 42 by 30 days”. No built-in admin UI for this yet.
“Affiliate didn’t get credit for the renewal”
Section titled ““Affiliate didn’t get credit for the renewal””EP Affiliate’s recurring commission setting must be enabled. If it’s off, only first-time conversions trigger commissions.
Feedback and corrections
Section titled “Feedback and corrections”For a quick question about this plugin, EP Support inside your admin is the fastest option. The chat widget sits on every EP plugin settings page and knows which one you’re on, with starter questions and links preloaded for that exact screen.
For anything bigger — a bug report, a feature request, or a “how do I…” that needs a real reply — open a ticket at help.elmspark.com. A real person, helped by AI, writes the reply. Usually within a few hours. Tickets don’t disappear into the void.
Changelog
Section titled “Changelog”0.2.30
Section titled “0.2.30”- Subscription emails now use EP Email’s sending method. The activated, payment received, payment failed and expired emails sent when Stripe reports a payment in the background, and expiry emails sent while you are in your admin, went through your server’s built-in mail instead of the sending method set in EP Email. It now goes out through EP Email, using the sending method you set there (Mailgun, Brevo, SMTP and so on), and shows in EP Email’s delivery log.
0.2.29
Section titled “0.2.29”- Settings language menu. The language menu in this plugin’s settings now lists only the languages it is actually translated into, plus English, so you can no longer pick a language that changes nothing.
- Danish. Adds a Danish translation.
0.2.28
Section titled “0.2.28”- Fixes the plugin not loading after the 0.2.27 update. On 0.2.27 PageMotor could not start EP Ecommerce Subscriptions and skipped it, so your site stopped taking subscription notices from Stripe and PayPal, and customers could not cancel from their account page. 0.2.28 loads normally.
- Your subscriptions, payments and settings were not changed. Stripe resends a notice your site could not take for up to three days, and each live PayPal subscription is checked with PayPal once a day, so what was missed is picked up once you update.
- Everything in 0.2.27 is included, among it the new “First payment refunded in full: stop the subscription from renewing” setting, off unless you switch it on.
0.2.27
Section titled “0.2.27”- New setting: “First payment refunded in full: stop the subscription from renewing”. It is off unless you switch it on, so nothing changes for you until you do.
- With it on, when you refund a customer’s first payment in full and they have paid nothing since, the subscription stops renewing, so they are not charged again:
- Stripe: the subscription ends at the close of its current period. If the refund then fails or is cancelled, the subscription carries on renewing as before.
- PayPal: the subscription is cancelled at PayPal. PayPal cannot restart it, so if that refund later fails you are told in the log, and the customer would need to subscribe again.
- If Stripe or PayPal cannot be reached at that moment, it is tried again within the hour.
- A subscription the customer has already cancelled, or one with a later payment that still stands, is left alone.
- With the setting off, you still cancel the subscription yourself if it should end, as before.
0.2.26
Section titled “0.2.26”- Fixed: customers could not cancel a PayPal subscription from their account page. On sites where the cancel request reached the site before PageMotor had set up the page, the plugin could not find EP Ecommerce PayPal. The customer was told “We could not cancel your subscription just now”, and PayPal was never asked. It now finds PayPal in every request.
- Scheduled PayPal payment syncs reach PayPal however your site loads its plugins. Update together with EP Ecommerce PayPal 0.1.21 (either order is safe).
- The same fix for Stripe subscriptions: Cancel, and the scheduled Stripe refund checks, now find EP Ecommerce Stripe however your site loads its plugins. Update together with EP Ecommerce Stripe 0.1.32 (either order is safe).
- The message under the Cancel button (“Cancels at end of period”, or why it could not be cancelled) is now read out by screen readers.
0.2.25
Section titled “0.2.25”- A PayPal subscription payment refunded in part is now recorded with the amount refunded. PayPal’s payment list only says a payment was partly refunded, not by how much. Now each refund is recorded from PayPal’s refund notice, with its exact amount and date, and several refunds of one payment add up. EP Finance Sources uses this to take the right amount out of your books.
- A payment refunded in full through several smaller refunds is marked fully refunded once they add up. If it was the first payment, its order is marked refunded, as for a single full refund.
- A refund notice for a payment your site has not recorded yet fetches that subscription’s payments from PayPal first.
- This needs EP Ecommerce PayPal 0.1.19, and the PAYMENT.SALE.REFUNDED event added to your PayPal webhook.
0.2.24
Section titled “0.2.24”- Refunding a subscription’s first payment in full now marks its order refunded and withdraws the access it gave. Until now the order made at checkout stayed “completed” and the customer kept their membership, because the payment services could not match the refund to that order. It now works the same way as a refunded shop order, for Stripe and PayPal.
- If the customer has already paid a later renewal, they keep the access that renewal paid for; only the order is marked refunded.
- If a Stripe refund of the first payment fails or is cancelled (the money stays with you), the order goes back to completed and the access comes back (needs EP Ecommerce 0.1.44 or later).
- A partial refund of the first payment, or any refund of a renewal, changes nothing here, as before.
- The subscription itself is not cancelled: it carries on at Stripe or PayPal. Cancel it there if it should end. A later payment gives the customer their access back.
0.2.23
Section titled “0.2.23”- A Stripe refund that fails or is cancelled no longer shows as refunded. Stripe can fail a refund after you make it (for example when the customer’s card has been closed) or cancel one that was still pending. The money then stays with you, but until now the payment kept showing as refunded. The payment now goes back to not refunded (or to the smaller amount still refunded), and the failed refund is kept on the payment with its status, so EP Finance Sources can put the money back in your books.
- Add the
charge.refund.updatedevent to your Stripe webhook endpoint for this plugin (the settings page lists it) so a failed refund is seen straight away. Without it, a refund that was still pending is checked with Stripe in the background every hour. - A refund that is still on its way (pending) keeps counting as refunded, as before.
0.2.22
Section titled “0.2.22”- Refunded Stripe subscription payments are now recorded as refunded. When you refund a Stripe subscription payment, in full or in part, the plugin now marks that payment as refunded, with the exact amount given back and each Stripe refund kept, so EP Finance Sources can take each refund out of your books for its own amount.
- Add the
charge.refundedevent to your Stripe webhook endpoint for this plugin (the settings page lists it) so refunds are seen straight away. Without it, refunds are still picked up by a daily check with Stripe. - A refund made in two steps (part now, the rest later) is recorded as partly refunded, then fully refunded, and the amount only ever goes up.
- Payments recorded before this update are linked to their Stripe payment in the background (a few at a time, each hour), so their refunds are found too.
0.2.21
Section titled “0.2.21”- Refunded PayPal subscription payments are now recorded as refunded. When you refund a PayPal subscription payment, the plugin now notices it (PayPal shows it on the payment, not as a separate notice) and marks that payment as fully or partly refunded, so EP Finance Sources can take a full refund back out of your books and list a partial one for you to enter.
- A payment that was refunded before the plugin first read it is still recorded as a payment taken, then marked refunded.
- Refunds made after a customer cancelled are caught too: a cancelled or ended PayPal subscription whose last payment was in the last 180 days is still checked with PayPal once a day.
- A data export for a customer now shows whether each subscription payment was refunded.
0.2.20
Section titled “0.2.20”- Every subscription payment is now kept on record. Until now the plugin remembered only a subscription’s current billing period, so EP Finance Sources could not record PayPal renewals in your books at all, and could miss a Stripe renewal if two came between its nightly runs. Each payment Stripe or PayPal takes is now stored with its date and the amount actually paid, and EP Finance Sources records them from there.
- Past PayPal payments are recovered from PayPal. Payments your PayPal subscribers made before this update are read back from PayPal a few subscriptions at a time, in the background, so on a site with many PayPal subscribers this can take several nights to finish.
- Each live PayPal subscription is also checked with PayPal once a day, so a payment whose notice from PayPal never arrived is still recorded.
- A trial or other free billing period is not recorded as a payment.
- A data export for a customer now includes their subscription payments. Erasing a customer’s data works as before: their subscriptions are anonymised, and the payment records, which hold no personal details, stay with them.
0.2.19
Section titled “0.2.19”- Stripe subscription notifications keep working while you change your webhook signing secret. When you roll the secret in Stripe, Stripe signs each notification with both the old and new secret for a while. The site only checked one of those signatures, so some genuine notifications could be turned away during that time. It now accepts the notification if either signature matches.
0.2.18
Section titled “0.2.18”- The Cancel button on “My Subscriptions” now works. On PageMotor 0.9 and later it did nothing, and there was no script behind it at all, so customers who were promised they could cancel at any time had no way to do it from your site. Clicking Cancel now asks for confirmation and cancels the subscription with Stripe or PayPal.
- With the default “Access continues until end of billing period” policy, Stripe stops renewing but the customer keeps access until the date they have paid up to, and they get an email saying so. With “Access revoked immediately”, the subscription ends at once.
- The subscription is only marked cancelled after Stripe or PayPal confirms it. If the provider refuses or cannot be reached, the customer sees “We could not cancel your subscription just now” and nothing changes, so nobody is told they are cancelled while still being billed.
- Customers no longer get a second “expired” or “cancelled” email when Stripe or PayPal confirms a cancellation they made on your site.
- PayPal cancellations now keep access until the paid-up date. PayPal ends a subscription the moment it is cancelled, and the plugin used to remove access at that same moment, while the email told the customer “You will retain access until immediately”. With the default “Access continues until end of billing period” policy, the customer now keeps access until the date they have paid up to, the membership ends by itself on that date, and the email gives that date. If PayPal cannot tell us the date, or the subscription has simply run its course, access ends straight away and the email says so plainly.
- The cancellation email no longer reads “retain access until immediately” when the end date is unknown. It now says access continues until the end of the current billing period.
- Subscription emails are now translated. The welcome, payment received, payment failed, cancelled and expired emails follow the site language in German, Spanish, French, Italian, Dutch and Portuguese, including the dates (for example “15. Oktober 2026” or “15 de octubre de 2026”). Until now every email went out in English whatever the site language.
- Email subjects read “Payment received: Yoga Club” rather than using a long dash.
- Prices in emails follow the site language. A German email shows “9,00 €” and “1.234,50 €”, French “1 234,50 €”, Dutch ”€ 9,00”. English is unchanged (“€9.00”), except that yen and other currencies without pence now show no “.00” (“¥1,000”), and “kr” or “CHF” no longer run into the number.
- No more duplicate receipts. Stripe sometimes delivers the same payment notice twice, for example after a slow response. Each repeat used to be treated as a new renewal, so a customer could get “Payment received” twice for one payment, or a false “Payment received” straight after their welcome email. Each payment is now handled once. A repeated “payment failed” notice is also dropped, while Stripe’s genuine next retry is still reported.
- PayPal: one welcome per subscription, and no free access after cancelling. PayPal can announce that a subscription is active more than once: as a repeat, or when a paused (suspended) subscription starts again. Each announcement used to create another membership, log consent again and send another welcome email, and a late repeat arriving after the customer had cancelled switched their access back on. Now the welcome happens once. A restarted subscription simply gets its access back without another welcome, and a repeat after cancellation is ignored.
- PayPal: one receipt per payment. A repeated PayPal payment notice used to send another “Payment received” email each time, and the first payment got a receipt straight after the welcome email that already showed it. Each payment now gets one email. A late payment notice arriving after the customer had cancelled also switched their access back on; it is now ignored.
- “No grace period” now means no grace period. Choosing it in the settings was silently treated as 7 days: customers kept access for a week after a failed payment, and the email told them so. Now access pauses as soon as a Stripe payment fails, and the email says access is paused until a payment goes through. If Stripe’s automatic retry then succeeds, access comes straight back. The 3, 7 and 14 day options work as before.
- Grace periods now end on time even if nobody visits the admin. A subscription whose payment had failed only lost access once its grace period ran out AND someone opened an admin page, so on a quiet site a non-paying customer could keep access for weeks. With EP Cron installed the check now runs every hour. Without it, the check runs at most once an hour whenever Stripe notifies your site of a payment event, as well as on admin page loads as before.
- Access comes back when a failed payment is later paid. If access had been removed after a failed payment, a successful retry used to leave the customer locked out even though they had now paid. The payment now restores their access.
- Renewals are no longer mistaken for new sign-ups. When Stripe’s notices arrived out of order, a renewal could be treated as a brand new subscription, sending a second welcome email and creating a duplicate membership. The plugin now uses Stripe’s own record of whether a payment is the first one or a renewal.
- Works on newer Stripe accounts. Stripe changed the shape of its data for accounts on its March 2025 API version or later. On such an account subscription checkout could fail, and payments reported to your webhook were ignored, so subscriptions never became active. The plugin now asks Stripe for the data shape it was built for on every request, and reads Stripe’s webhook messages in either shape, whatever version your webhook endpoint is set to.
- Receipts show what the customer was actually charged. The welcome, payment received and payment failed emails used to show the product’s list price whatever happened, so a customer charged €10.80 including VAT, or €4.50 with a coupon, got an email saying €9.00. They now show the amount Stripe charged (or is trying to charge) in the currency it was charged in, and for PayPal the amount of the last payment PayPal reports. If the provider does not give an amount, the email shows the list price as before.
- The welcome email greets customers by their name. It uses the name they gave at checkout (“Welcome, Anna Müller!”) instead of the first part of their email address (“Welcome, j.smith92!”). When there is no name it simply says “Welcome!”.
- The new cancel messages are translated into German, Spanish, French, Italian, Dutch and Portuguese, as is the “security check failed” message a customer can see if their page has gone stale.
0.2.17
Section titled “0.2.17”- Fixes Stripe subscription events being rejected. Since 0.2.13, when the Stripe signing secret started being stored encrypted, the subscription webhook read it back as blank and answered every Stripe event with “Webhook secret not configured”. New subscriptions stayed “incomplete” after the customer paid, renewals and failed payments were not recorded, and cancellations made in Stripe did not remove access. The webhook now decrypts the secret before checking the signature.
- Nothing to re-enter. Stripe retries a rejected event for up to three days, so recent events should arrive again by themselves. For anything older, check Developers, Webhooks in your Stripe Dashboard and resend failed events to this endpoint.
0.2.16
Section titled “0.2.16”- Fixes stored keys and passwords reading as empty after a PageMotor 0.11.3 or 0.11.4 update. After the core update, every secret this plugin had encrypted at rest came back blank, so anything that needed it failed with an authentication error until the value was typed in again. Nothing was deleted: the encrypted value was still in the settings row, but PageMotor 0.11.3 moved the site secret that opens it, and this plugin was still looking in the old place. It now finds the secret in both places, so an existing value opens again without re-entry, and a value that was re-entered in the meantime keeps working and is moved back under the site secret.
- If you updated PageMotor and then re-entered a key or password, there is nothing to do. If you updated and have not re-entered it, this release restores it on the next page load.
0.2.15
Section titled “0.2.15”- Fixed: on a site where this plugin was switched on but its settings page had never been opened, every membership-gated page broke for signed-in visitors only. Protected pages returned an error to anyone logged in, while visitors who were not signed in saw the normal “members only” prompt. That asymmetry is the dangerous part: the site owner, usually signed in, sees a broken page the public cannot see, or the reverse, depending on who happens to look.
- The cause was this plugin reading a database table it had not yet created. Worse, it took the whole membership system down with it, not just its own part, because the shared gate asks every subscription plugin before deciding. It now checks the table exists first and simply has no opinion when it does not.
- The same fault in a second form on the subscriptions shortcode produced a page that cut off halfway while still reporting success, so uptime monitoring saw nothing wrong. That is fixed the same way.
- Three other reads also depended on a table belonging to a different plugin purely to show a product name. They no longer do, and the name is simply left out when it is not available.
- The plugin now creates what it needs when it needs it, so a Stripe payment or a front-end checkout can be the first thing it ever does on a site. It no longer depends on someone having opened the settings page first.
- Verified on a test site with the table deliberately removed: gated pages went from an error to a complete page, the subscriptions shortcode from a truncated page to a complete one, and access decisions stayed correct throughout.