EP Ecommerce Stripe
EP Ecommerce Stripe handles Stripe payments for EP Ecommerce. It uses raw cURL against the Stripe API, so no Composer dependencies, and implements the full payment lifecycle: PaymentIntent creation with idempotency keys, webhook signature verification, payment confirmation fallback for delayed webhooks, and admin-initiated refunds.
Published by ElmsPark Studio.
Overview
Section titled “Overview”- PaymentIntents, not Charges. The modern Stripe approach, compatible with SCA and 3D Secure out of the box.
- Stripe Payment Element on the frontend (Stripe.js v3) with the default styled UI.
- Webhook-driven fulfilment with HMAC-SHA256 signature verification (timing-safe) for
payment_intent.succeededandpayment_intent.payment_failed. - Confirmation fallback for cases where the webhook is delayed — prevents a visible “waiting” state for the customer.
- Atomic order claiming so a webhook and a confirmation cannot double-fulfil.
- Admin refunds from the orders list via the Stripe Refunds API.
- Refund-driven revocation (0.1.22): a full refund made in the Stripe dashboard reaches your site as
charge.refundedand the plugin that fulfilled the sale is asked to take the entitlement back. - Zero-decimal currency handling (JPY, KRW, etc. — no implicit cents).
- Idempotency keys on every request so network retries never double-charge.
- Customisable statement descriptor with
{product_name}placeholder for bank statement clarity.
Requirements
Section titled “Requirements”- PageMotor 0.8.2b or later
- EP Ecommerce (base plugin)
- EP Suite base class
- EP Ecommerce Products for the checkout UI
- A Stripe account with API keys
Installation
Section titled “Installation”- Install EP Ecommerce and EP Ecommerce Products first.
ep-ecommerce-stripe.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.
Setting up Stripe
Section titled “Setting up Stripe”Step 1 — Get your API keys
Section titled “Step 1 — Get your API keys”- Log in to your Stripe dashboard.
- Under Developers → API keys, copy your publishable key and secret key.
- Stripe gives you two sets: one for test mode (prefixed
pk_test_/sk_test_) and one for live mode (pk_live_/sk_live_).
Step 2 — Configure the plugin
Section titled “Step 2 — Configure the plugin”Open Plugin Settings → EP Ecommerce Stripe.
- Enable Stripe payments. Toggle on.
- Mode. Test (for development) or Live (for production).
- Test API keys. Paste both.
- Live API keys. Paste both when ready to go live.
Step 3 — Set up the webhook
Section titled “Step 3 — Set up the webhook”Stripe pushes payment status to your server via a webhook. Without it, orders can stall.
- In your Stripe dashboard, go to Developers → Webhooks.
- Click Add endpoint.
- Endpoint URL:
https://yoursite.com/?ep_ecommerce_stripe_webhook=1 - Events to send:
payment_intent.succeededpayment_intent.payment_failedcharge.refunded(0.1.22, if you want a full refund to withdraw what was sold)
- Click Add endpoint.
- On the endpoint detail page, reveal the Signing secret (starts
whsec_). - Back in the plugin settings, paste it into Webhook signing secret.
Test the webhook from Stripe’s dashboard by sending a test event.
How the payment flow works
Section titled “How the payment flow works”- Customer clicks the checkout button on your site.
- Plugin creates a Stripe Customer (or looks up an existing one by email).
- Plugin creates a PaymentIntent for the product amount with idempotency key.
- Frontend renders the Payment Element from Stripe.js using the PaymentIntent’s client secret.
- Customer enters card details. Stripe handles 3D Secure if required.
- On success, Stripe fires
payment_intent.succeededto your webhook. - Plugin atomically claims the order, marks it paid, triggers fulfilment (grant membership, generate license, etc.).
- As a fallback, the frontend also calls a confirmation endpoint — if the webhook is delayed, the confirmation completes the flow.
- Customer sees success message, receives EP Email confirmation.
Refunds
Section titled “Refunds”From the EP Ecommerce orders list, any paid Stripe order has a Refund button:
- Full refund. Refunds the entire charge.
- Partial refund. Enter an amount less than or equal to the charge.
- The order status updates to Refunded / Partially Refunded.
- Fulfilment reversal for EP Ecommerce’s own products (revoking memberships, voiding licences) is on you, because business rules vary — by default the refund only handles the money.
Refunds made in Stripe (0.1.22)
Section titled “Refunds made in Stripe (0.1.22)”A full refund issued from the Stripe dashboard arrives as a charge.refunded webhook. The plugin matches it to the original sale from its own fulfilment record and hands it to the plugin that sold the item, which can then withdraw what it granted. EP Courses does this for course enrolments. A partial refund is logged and ignored, since it is usually a goodwill gesture rather than an undoing of the sale. Chargebacks are not acted on, because a dispute can be won.
Add charge.refunded to the events your webhook sends if you want this.
Statement descriptor
Section titled “Statement descriptor”What appears on the customer’s bank statement. Defaults to your business name (as configured in Stripe), but can be overridden per-product:
{product_name}is substituted with the actual product name at charge time.- Stripe limits descriptors to 22 characters. Anything longer gets truncated.
Security
Section titled “Security”- Webhook verification uses Stripe’s timing-safe HMAC comparison, so a timing-attack cannot reveal your signing secret.
- CSRF verification on frontend AJAX requests.
- IDOR protection on the confirmation endpoint — customer A cannot confirm customer B’s order.
- Keys never in the database unencrypted. Secret keys are stored as settings, admin-only readable.
Troubleshooting
Section titled “Troubleshooting”“Webhook signature verification fails”
Section titled ““Webhook signature verification fails””The webhook signing secret you pasted doesn’t match the one in your Stripe dashboard. Rotate the secret in Stripe, paste the new one, test again.
“Orders are showing as Paid but memberships aren’t being granted”
Section titled ““Orders are showing as Paid but memberships aren’t being granted””Fulfilment happens after webhook verification. If webhook verification passed but fulfilment didn’t, check error logs — commonly a database constraint error or a missing product reference. Paste the order ID into the review queue.
“Test mode works, live mode fails”
Section titled ““Test mode works, live mode fails””Live-mode keys must be from the same Stripe account as your test keys. Mixing up accounts is a common cause. Also check that live mode is enabled in your Stripe account (some new accounts start with a hold).
“The Payment Element shows an error about invalid publishable key”
Section titled ““The Payment Element shows an error about invalid publishable key””The publishable key in the settings doesn’t match the mode. If mode is Live but you pasted a test key, Stripe rejects. Check the prefix: pk_live_ for live, pk_test_ for test.
“Two products on one page and the customer was charged for the wrong one”
Section titled ““Two products on one page and the customer was charged for the wrong one””Fixed in 0.1.22: each checkout form on a page now keeps its own payment state. Update the plugin. If a customer still sees it after the update, their browser is holding the old script; 0.1.23 changed how the script is loaded so that a release replaces the cached copy, and that takes effect with the next release after it.
“Customers complain about 3D Secure prompts”
Section titled ““Customers complain about 3D Secure prompts””3DS is triggered by Stripe based on risk and issuer rules. The Payment Element handles it automatically. If customers can’t complete 3DS, it’s usually their issuer or a network issue, not your site.
Changelog
Section titled “Changelog”0.1.34
Section titled “0.1.34”- 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.1.33
Section titled “0.1.33”- A refund that failed no longer takes a customer’s purchase away when Stripe’s messages arrive out of order. Stripe does not always send its notices in order. When the “refund failed” notice arrived before the “charge refunded” one, the customer kept their purchase at first, then lost it for good when the late notice came, even though the refund never happened. Now, before taking anything back, the plugin checks with Stripe that the payment really is still refunded. This covers shop orders and sales made through Stripe Checkout (events and courses).
- If Stripe cannot be reached at that moment, nothing is taken back, and Stripe is asked to send the notice again later.
0.1.32
Section titled “0.1.32”- Customers can always cancel a Stripe subscription from their account page. EP Ecommerce Subscriptions now finds this plugin directly, however your site happens to load its plugins. Before, if EP Ecommerce loaded after this plugin, pressing Cancel told the customer “We could not cancel your subscription just now” and Stripe was never asked. The same applied to the scheduled refund checks.
- Update together with EP Ecommerce Subscriptions 0.2.26 (either order is safe).
0.1.31
Section titled “0.1.31”- A booking or course refunded before its payment arrived no longer waits for ever. Since 0.1.30, a payment refunded before it was handed over gives nothing. EP Events and EP Courses are now told about it, so the booking or enrolment that was waiting for that payment shows as refunded instead of pending. This needs EP Events 1.0.69 and EP Courses 0.5.13; with older versions they stay pending, as in 0.1.30.
- If that refund later fails or you cancel it, the booking is confirmed or the course is opened as usual.
0.1.30
Section titled “0.1.30”- A refund that Stripe reports before the sale itself no longer gives the place or the course away. Stripe does not always send its messages in order. If you refund a Stripe Checkout sale (an EP Events ticket, an EP Courses course) very quickly, the refund can arrive before the sale has been handed to EP Events or EP Courses. Before, the refund was ignored, because nothing had been handed over yet, and the sale was then handed over anyway: the customer got their place or course and their money back.
- Now the refund is remembered. Before the sale is handed over, Stripe is asked whether the money is still refunded. If it is, nothing is given, and the sale is marked as refunded.
- If that refund later fails or you cancel it, the money stays with you, so the sale is handed over after all.
- The booking or enrolment that was waiting for payment stays as it was, waiting. Nothing is emailed to the customer.
0.1.29
Section titled “0.1.29”- Course and event places bought through Stripe’s hosted checkout are only given once the payment has actually arrived. Card payments work exactly as before. Slower payment methods, such as bank debits, can finish checkout before the money arrives; until now the place was given straight away, even if the payment later failed. Now the place is given when Stripe confirms the money has arrived, and never if the payment fails.
- A place is never given twice for one payment. If Stripe sent the same “checkout completed” message again under a new reference, the booking or course could be handed over a second time. It is now handed over once.
- If you use a slower payment method, add two events to your Stripe webhook:
checkout.session.async_payment_succeededandcheckout.session.async_payment_failed. The settings screen lists them. Card-only shops need no change.
0.1.28
Section titled “0.1.28”- Event tickets sold through Stripe’s hosted checkout come back when their refund fails. When you refund a ticket in full, the booking is withdrawn. If Stripe then fails the refund (for example because the customer’s card has been closed) or you cancel it while it is still pending, the money stays with you, so the booking is now given back automatically: it is confirmed again and the ticket works again. This needs EP Events 1.0.67 or later.
- It works however many times the same payment is refunded and the refund fails, and a refund that only covered part of the price changes nothing, as before.
- Other plugins that sell through Stripe’s hosted pages can now take part. Until they do (EP Courses today), a failed refund of their sale is recorded in the Stripe fulfilment queue as something to put right by hand, and nothing else changes.
- Keep
charge.refund.updatedturned on in your Stripe webhook (added in 0.1.26): without it a failed refund is not noticed.
0.1.27
Section titled “0.1.27”- The Refund button now takes back what the order gave, just as a refund made in Stripe does. Refunding an order from your admin used to mark it refunded but leave the customer’s membership, download links and licence keys working. Now they are withdrawn straight away. If Stripe later fails or cancels that refund, the order and everything it gave come back automatically (with EP Ecommerce 0.1.44 or later).
- A refund Stripe refuses on the spot no longer marks the order refunded. You see “Stripe could not complete this refund” and the order stays as it was.
0.1.26
Section titled “0.1.26”- A Stripe refund that fails or is cancelled now gives your customer back what the refund took. 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, and the money then stays with you. Until now the order stayed refunded and the customer stayed locked out. Now the order is marked completed again and the membership, download links and licence keys the refund took are given back. This needs EP Ecommerce 0.1.44 or later.
- A partial refund that fails changes nothing, as partial refunds never took anything away.
- Add one event to your Stripe webhook:
charge.refund.updated. The settings screen now lists all five events to turn on in Stripe. Without it, a failed refund is not noticed. - Tickets and courses sold through Stripe’s hosted pages (EP Events, EP Courses) are not given back automatically when a refund fails; restore those by hand.
0.1.25
Section titled “0.1.25”- A payment is now only accepted for the order it was taken for. If your Stripe account is shared with another shop (a second site, or an existing WooCommerce store), a payment made there could complete an unpaid order here that happened to have the same order number, and deliver the product for free. Payments are now matched to the exact Stripe payment created for each order.
- The payment confirmation step after checkout now only checks the payment belonging to that order.
- The checkout never says “Thank you for your purchase” unless the purchase actually happened. When another payment provider on the same page took the order, this plugin’s checkout used to show the thank-you message although nothing had been charged. It now hands the buyer to that provider, or shows an error.
- Refunding a product order in Stripe now takes back what it granted. A full refund used to leave the buyer with their membership, download and licence. Now the order is marked refunded and all of it is withdrawn. Partial refunds are still left alone, as before.
- The settings screen now lists all four webhook events to turn on in Stripe:
payment_intent.succeeded,payment_intent.payment_failed,charge.refundedandcheckout.session.completed. If you set up your webhook from the old list, add the last two in your Stripe Dashboard. - Event tickets in Japanese yen (and other currencies without pence or cents) are now charged the right amount. A ¥1,000 ticket bought through EP Events’ Stripe checkout was charged ¥100,000. It is now charged ¥1,000. Pounds, dollars, euros and other currencies were never affected.
- Payment methods that open the buyer’s bank or wallet now work. If you have iDEAL, Bancontact, Klarna or a similar method switched on in Stripe, buyers who chose it used to get an error instead of being sent to approve the payment. They are now sent on, and brought back to the same page, where the checkout tells them whether the payment went through.
- A payment that takes time to clear (a bank debit, for example) now says it is being processed instead of thanking the buyer for a completed purchase.
- Discount codes. A new Discount codes setting (off until you tick it) lets buyers use promotion codes you create in Stripe or with EP Stripe Discount Codes. “Product checkout” adds a discount code box to the checkout form: the code is checked with Stripe, the buyer sees what it takes off, and the receipt shows it. “Stripe hosted pages” switches on Stripe’s own code box for EP Events and EP Courses bookings.
- On the product checkout, codes work for card payments. Expiry dates, minimum spend, first-order-only and use limits are all respected. A code limited to one Stripe customer or to particular Stripe products, or one that takes the whole price off, is refused with a message.
- The README now lists all four webhook events this plugin handles.
- Stripe payment 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.1.24
Section titled “0.1.24”- 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.1.23
Section titled “0.1.23”7 September 2026. The checkout script loads only on pages carrying a checkout, not sitewide, and is served with a version stamp so browsers pick up each release instead of a cached copy.
0.1.22
Section titled “0.1.22”2 September 2026. Full refunds made in Stripe can take an entitlement back through the plugin that sold it. Two checkout forms on one page no longer share payment state, which could charge the customer for the wrong product.
0.1.19 to 0.1.21
Section titled “0.1.19 to 0.1.21”1 September 2026. Stripe Checkout sessions for other EP plugins that sell through this one (EP Courses uses this), with confirmation and webhook fixes, and a fix for two plugins bundling the shared Stripe helper taking the site down on activation.
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.