Saga Payments for WooCommerce

Description

Saga Payments for WooCommerce is a full-featured payment gateway built for Nordic merchants. Accept secure payments through multiple payment methods with enterprise-grade features:

Payment Methods

  • Card Payments – Visa, Mastercard, American Express, Discover
  • Vipps – Norway’s most popular mobile payment app
  • Klarna – Buy now, pay later
  • Apple Pay – Fast checkout for Apple users
  • Google Pay – Fast checkout for Android users
  • Swish – Sweden’s most popular mobile payment app
  • MobilePay – Denmark and Finland’s popular mobile payment app

Features

  • Easy Setup – Just enter your Merchant ID, Terminal ID, and Public Key
  • Saved Payment Methods – Customers can save cards for faster checkout
  • WooCommerce Subscriptions – Full support for recurring payments with automatic renewals
  • WooCommerce Pre-Orders – Charge customers when pre-ordered products become available
  • Authorize & Capture – Authorize payments and capture later, or capture immediately
  • Auto-Capture – Automatically capture when order status changes to Processing/Completed
  • Auto-Void – Automatically void authorizations when orders are cancelled
  • Express Checkout – Apple Pay/Google Pay buttons on product pages and cart
  • WooCommerce Blocks – Full support for Blocks Checkout including saved cards
  • HPOS Compatible – Full support for High-Performance Order Storage
  • Refunds – Full and partial refunds directly from WooCommerce admin
  • Professional Design – Clean, responsive payment widget

Plugin Integrations

  • WooCommerce Subscriptions – Automatic recurring payments
  • WooCommerce Pre-Orders – Charge on release
  • WooCommerce Blocks – Full Blocks checkout support
  • WooCommerce HPOS – High-Performance Order Storage
  • Saga Product Sync – Bidirectional product catalogue synchronisation
  • Compatible with all major WordPress themes

Product Sync (Saga)

Automatic bidirectional synchronisation between your WooCommerce store and Saga:

  • Saga is master – pull products, prices, images, VAT rates and stock from Saga into WooCommerce
  • Push changes back – WooCommerce product edits are automatically pushed to Saga in real-time
  • Inventory sync – relative stock deltas (STOCK_DOWN / STOCK_UP) on order completion, refund and cancellation
  • Auto-retry – failed inventory adjustments are queued and retried every 5 minutes (up to 20 attempts)
  • Full field mapping – prices (øre kroner), VAT rates (25 / 15 / 12 / 0 %), units, barcodes, cost price, images
  • Admin panel – WooCommerce -> Saga Product Sync with connection test, manual sync, and status dashboard
  • Configure via WooCommerce -> Saga Product Sync

Requirements

  • WordPress 5.8 or later
  • WooCommerce 7.0 or later
  • PHP 7.4 or later
  • SSL certificate (HTTPS)
  • Saga Payments merchant account

External services

This plugin relies on the following external services to process payments. A Saga Payments merchant account is required.

Saga Payments API

All payment operations (order creation, payment verification, refunds, captures, voids, and subscription management) are handled through the Saga Payments API proxy server. When a customer initiates a checkout, order data including amount, currency, line items, and customer billing/shipping information is sent to this service via server-side HTTP requests.

  • Service URL: https://sagapay-api-v3.kristoffer-afc.workers.dev/api
  • Provider: Saga Payments (Cloudflare Worker proxy to Surfboard Payments)
  • Website: https://sagapay.no
  • Terms of Service: https://www.sagapay.no/terms-of-service
  • Privacy Policy: https://www.sagapay.no/personvernserklaering

Saga Payments Merchant Dashboard and Documentation

The plugin settings screen links store administrators to Saga Payments dashboard and documentation pages for API key, webhook, and product catalogue setup. These links open only when an administrator clicks them. No customer or payment data is sent automatically by these documentation links.

  • Dashboard URL: https://dashboard.sagapay.no
  • API key documentation URL: https://docs.sagapay.no/guides/getting-started/opprett-api-nokler
  • Webhook documentation URL: https://docs.sagapay.no/guides/getting-started/webhooks
  • Product catalogue documentation URL: https://docs.sagapay.no/api-reference/api-reference/product-catalogue/
  • Provider: Saga Payments
  • Terms of Service: https://www.sagapay.no/terms-of-service
  • Privacy Policy: https://www.sagapay.no/personvernserklaering

Saga OTP Verification Service

When a guest customer pays with a previously saved card, the plugin sends a one-time password (OTP) verification email via a Cloudflare Worker. Only the customer’s email address, a generated OTP code, the store name, and the cart total are sent in the request. No card data is transmitted. The OTP email is delivered via the worker using the Resend email service.

  • Service URL: https://saga-otp-worker.kristoffer-afc.workers.dev
  • Provider: Saga Payments (Cloudflare Worker)
  • Website: https://sagapay.no
  • Terms of Service: https://www.sagapay.no/terms-of-service
  • Privacy Policy: https://www.sagapay.no/personvernserklaering

Saga Regnskap (Accounting) Integration

When the store owner completes merchant OTP verification (or clicks the “Koble til regnskap” button in the gateway settings), the plugin sends a one-time registration request to the Saga Regnskap accounting service so the merchant’s bookkeeping can be reconciled against the store. Only store-level facts are sent: the Surfboard merchant ID, the store URL, the activation reference, plugin/WordPress/WooCommerce versions, currency, tax display setting and which gift card system is installed. No customer data, order contents or payment credentials are transmitted. After registration, the accounting service reads order and refund TOTALS (amounts, tax rates, Saga payment references — never names, emails, phones, addresses or IPs) from a read-only, token-authenticated REST endpoint served by this plugin at /wp-json/saga-accounting/v1/. This applies only to merchants with a Saga Regnskap agreement; stores without an agreement store nothing.

  • Service URL: https://regnskap.sagapay.no (fallback: https://saga-regnskap-api.kristoffer-afc.workers.dev)
  • Provider: Saga Payments (Cloudflare Worker)
  • Website: https://sagapay.no
  • Terms of Service: https://www.sagapay.no/terms-of-service
  • Privacy Policy: https://www.sagapay.no/personvernserklaering

Surfboard Online SDK

The payment form displayed on your checkout page is rendered by the Surfboard Online SDK. This JavaScript library is loaded from an external server into the customer’s browser and handles the secure payment UI including card input fields, Vipps/MobilePay popups, Klarna widget, and Apple Pay / Google Pay buttons. Card data is entered directly into Surfboard-hosted iframes and never touches your server.

  • SDK URL: https://thorium.surfgw.com/OnlineSDK.js
  • Provider: Surfboard Payments
  • Website: https://www.surfboardpayments.com
  • Terms of Service: https://www.surfboardpayments.com/terms-and-conditions
  • Privacy Policy: https://www.surfboardpayments.com/privacy-policy

Surfboard Hosted Payment Page

When the plugin is configured in redirect mode, or for certain subscription flows, the customer’s browser is redirected to a hosted payment page operated by Surfboard Payments. The order amount, currency, and return URLs are passed via the redirect. All payment data is entered on the hosted page.

  • Service URL: https://pay.withsurfboard.com
  • Base URL used by redirects: https://pay.withsurfboard.com/
  • Provider: Surfboard Payments
  • Terms of Service: https://www.surfboardpayments.com/terms-and-conditions
  • Privacy Policy: https://www.surfboardpayments.com/privacy-policy

Surfboard Vipps/MobilePay Intermediary

For Vipps and MobilePay payment flows, the Surfboard SDK loads an intermediary page in an iframe to handle the mobile payment authorization. This is managed automatically by the Surfboard Online SDK and serves as the bridge between your store and the Vipps/MobilePay apps.

  • Service URL: https://vipps.withsurfboard.com
  • Provider: Surfboard Payments
  • Terms of Service: https://www.surfboardpayments.com/terms-and-conditions
  • Privacy Policy: https://www.surfboardpayments.com/privacy-policy

Google Pay API

When Google Pay is enabled as a payment method, the Google Pay JavaScript SDK is loaded in the customer’s browser to render the Google Pay button and handle the payment sheet. Payment token data is returned to your store and forwarded to the Saga Payments API for processing.

  • SDK URL: https://pay.google.com/gp/p/js/pay.js
  • Provider: Google
  • Website: https://pay.google.com
  • Terms of Service: https://payments.google.com/payments/apis-secure/get_legal_document?ldo=0&ldt=googlepaytos
  • Privacy Policy: https://policies.google.com/privacy

QR Code Generation

For Swish payments, if the payment gateway does not return a pre-rendered QR code image, the plugin generates a QR code locally using the bundled qrcode-generator library (MIT license, by Kazuhiko Arase). No external service is called for QR code generation.

Saga Checkout SDK (Diagnostics Only)

The plugin diagnostics page displays the configured SDK endpoint for admin troubleshooting purposes. The actual payment SDK loaded during checkout is the Surfboard Online SDK documented above.

  • Provider: Saga Payments
  • Website: https://sagapay.no
  • Terms of Service: https://www.sagapay.no/terms-of-service
  • Privacy Policy: https://www.sagapay.no/personvernserklaering

Saga Product Sync API

If the optional product synchronization feature is enabled, the plugin sends product data (name, description, price, SKU, images, categories) from your WooCommerce catalog to the Saga Payments product API for catalogue management. This is an opt-in feature configured in the plugin settings.

  • Service URL: https://api.sagapay.no
  • Provider: Saga Payments
  • Terms of Service: https://www.sagapay.no/terms-of-service
  • Privacy Policy: https://www.sagapay.no/personvernserklaering

Payment Method Logos

Payment method logos (Visa, Mastercard, Amex, Vipps, Klarna, Apple Pay, Google Pay, Swish, MobilePay) are bundled locally within the plugin in SVG format. No external CDN is used for logo display.
SVG files and inline SVG markup use the standard SVG namespace URI http://www.w3.org/2000/svg and xlink namespace URI http://www.w3.org/1999/xlink as static XML identifiers; these are not external network requests.

Installation

  1. Upload the saga-payments folder to the /wp-content/plugins/ directory
  2. Activate the plugin through the ‘Plugins’ menu in WordPress
  3. Go to WooCommerce > Settings > Payments > Saga Payments
  4. Enter your Merchant ID and Store ID (from your Saga Payments dashboard at https://dashboard.sagapay.no) and click Save changes
  5. The plugin will automatically provision your Terminal ID and Public Key on save – no manual key handling required
  6. Enable the payment method and save

FAQ

How do I get a Saga Payments account?

Contact Saga Payments at support@sagapayments.com to set up your merchant account.

Does this support test mode?

Yes! Enable test mode in the plugin settings to test without processing real payments.

Which currencies are supported?

The plugin supports NOK (Norwegian Krone), SEK (Swedish Krona), DKK (Danish Krone), EUR and other currencies supported by Saga Payments.

Can customers save their cards?

Yes! When “Enable Saved Cards” is turned on in settings, logged-in customers can save their cards for faster future checkout.

Does this work with WooCommerce Subscriptions?

Yes! Full integration with WooCommerce Subscriptions for automatic recurring payments, payment method changes, and subscription lifecycle management.

Can I authorize and capture later?

Yes! Set “Capture Mode” to “Manual” in settings. Payments will be authorized at checkout and you can capture from the order page when ready to ship.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“Saga Payments for WooCommerce” is open source software. The following people have contributed to this plugin.

Contributors

Changelog

4.0.724

  • Product sync: the short-read check from 4.0.723 now measures before it enforces. What the server’s declared total actually counts – products, variants, or every store – has not yet been read from a live catalogue, and if it counts more than the listing, discarding every “short” read would have stopped the pull for every shop. So by default a short read is now logged at error level, with both numbers, and nothing is discarded; the guard that stops a sync-retired product being deleted from the catalogue keeps a wrong retirement non-destructive in the meantime. Discarding can be switched on with the saga_catalog_refuse_short_reads filter once the header is understood. A shop whose products keep returning to the trash will now have the reason in its log after one pull: “declared N products but only M were read”.

4.0.723

  • Product sync: a catalogue read that comes up short is now detected instead of being treated as the whole catalogue. Surfboard declares its total in a response header that the plugin never read, so a server that ignored the page header and answered every page with page one was indistinguishable from a complete read – and the orphan check then judged every product not on that page as deleted upstream and moved it to the trash. A merchant restoring those products found them back in the trash after the next pull, because the read was short every time. A short read is now treated exactly like a failed page: nothing is retired on the strength of it, and the log says how many the server declared against how many were read. When the server declares no total, nothing changes.
  • Product sync: a product the sync itself retired is no longer deleted from the catalogue when WordPress purges the trash. WordPress empties the trash after 30 days on a cron; the plugin hooked that to the same handler as a merchant’s own deletion, whose only guard did not apply on a cron, so every product the sync had parked in the trash was a month later deleted from the real POS catalogue by the plugin. If the retirement was right the delete was noise; if it was wrong – a short read, a catalogue migration – it destroyed a product the till still sold. The push delete-sweep had already been fixed for exactly this after an earlier incident; this hook was the other door to the same room. Both now key on the same stamp.
  • The retirement stamp is cleared when the catalogue lists the product again, so a product that came back does not carry it for life and have the merchant’s own later deletion of it refused.

4.0.722

  • A VAT rate nobody set is no longer printed as “0 %”. A line with no tax information at all – tax switched off in the shop, or a line from before it was configured – now shows a dash and the summary reads “Merverdiavgift” without a rate. Printing 0 % put a rate on a legal document that the shop never stated, and made it indistinguishable from a genuine zero rate. A configured 0 % rate is a statement and still prints as one.
  • When the seller is not registered in the VAT register, the document says so. A document with no VAT and no stated reason reads as an omission; that one line is what makes the figures make sense.

4.0.721

  • Sales documents can now be switched on. Everything the series needs is in place: the seller resolved from the Surfboard merchant and refreshed daily, a number series keyed per legal entity and year, annulment and replacement for a document issued with an error, the Foretaksregisteret marking and head-office address for a limited company, and the reverse-charge wording. The setting is still off by default and a shop must also have an accounting agreement, so nothing starts issuing on its own.
  • Two things are still unproven and are stated here rather than left to be discovered: the reverse-charge trigger has never fired on a real cross-border B2B sale, and the seller comes from whichever Surfboard merchant a shop is paired to – a shop paired to the wrong merchant will get documents naming the wrong company, correctly resolved and wrong.

4.0.720

  • The seller identity is now refreshed daily rather than captured once. A company can be renamed, move, change its VAT registration or enter liquidation – and “Under avvikling” is exactly what the regulation requires the document to state. An identity captured only at pairing would have been frozen for the life of the shop and printed on every document it ever issued, with nothing to say it had gone stale. There is also a “Hent selger på nytt” button for when a change should take effect immediately.
  • No credential is sent to fetch it: the accounting backend proves the shop by calling back to this site’s own handshake endpoint, the same challenge-response the pairing uses, so nothing is stored in the clear and the pairing token is not rotated to read an identity.
  • Issuing never waits on that lookup. Documents are rendered from what is stored, so a sales document cannot fail because the accounting backend is unreachable, and a failed lookup never clears an identity already held.

4.0.719

  • The seller on a sales document is no longer typed into a settings field. It is resolved from the Surfboard merchant and handed over by Saga Regnskap when the shop pairs – the same chain the accounting side uses, so the two can never name different sellers for one sale. A typed field was one typo away from producing two valid documents with two different sellers for the same sale, each looking correct.
  • Documents from a limited company now carry the word “Foretaksregisteret” and the company’s registered head-office address, as bokf.forskr. 5-1-2 annet ledd requires, and say “Under avvikling” when that applies. The Foretaksregisteret marking keys on the company form (AS, ASA, NUF), not on mere presence in the register – a sole proprietorship can be registered without the wording covering it. When the company form is not known, nothing is printed: a register claim that cannot be told from a checked one is worse than a missing one.
  • Still switched off and unable to be switched on. Everything the series needs is now built, but no shop has actually received a seller identity yet, so none of it has issued a real document.

4.0.718

  • Sales documents now carry the reverse-charge marking when it applies: the exact words bokf.forskr. 5-1-1 nr. 7 prescribes, plus the buyer’s VAT registration number. The automatic test is deliberately narrow – no VAT charged, a buyer VAT number recorded, and a buyer in another country – because printing the marking on a sale it does not cover is as wrong as leaving it off. A VAT plugin can decide it either way through the saga_sales_document_reverse_charge filter, which is also how a domestic reverse-charge sale is declared.

4.0.717

  • Internal naming fix with a real hazard behind it: the “who issues” setting stored the value “saga” for “this plugin issues”, while the accounting engine’s equivalent setting uses “saga” for “the accounting engine issues”. The same word pointed opposite ways in the two systems, and anyone later syncing the two lists by matching like word to like would have inverted both. The value here is now “plugin”, which cannot be misread. A shop that had already stored the old value is read as “plugin” rather than being quietly reset to automatic. The wire field that tells the accounting integration who issued a given document is a separate contract and is unchanged.

4.0.716

  • The shop now chooses who issues its sales documents, instead of the plugin guessing. Automatic detection still picks the default – if WooCommerce PDF Invoices & Packing Slips, YITH PDF Invoice or a couple of others are active, they issue and Saga Payments keeps out of the way – but the list of invoice plugins can never be complete, and guessing wrong fails silently in both directions. An invoice plugin we do not recognise would mean both issue: two documents with two number series for one sale, and spent numbers cannot be cleaned up. One we do recognise but which only prints packing slips would mean nobody issues and nothing says so. The setting is under Saga Payments, and it states plainly what the current choice will do.
  • When another plugin is the issuer, Saga Payments attaches nothing to the customer’s e-mail and issues nothing; the accounting integration is served that plugin’s document instead. If it has not produced one, the answer is that there is none – never a substitute of ours.
  • Fix in the accounting API: orderCountTotal in /status counted every order status while /orders deliberately omits checkout drafts, which are abandoned carts rather than orders. A reader comparing the two saw orders it could never fetch and had no way to tell whether its own paging had lost them – the gap was 200 on the shop where this was found. The count now matches what the list returns, and the excluded drafts are reported separately as checkoutDraftCount.

4.0.715

  • The sales-document series can now be corrected. A document issued with a wrong VAT rate, buyer or amount can be annulled and replaced: the annulled snapshot stays with its reason (nothing is deleted – five-year retention, and a spent number that can be explained), the replacement gets a new number and prints which document it supersedes, and any credit note that pointed at the annulled document is annulled and reissued against the standing one. A reason is mandatory; without it the annulment is refused, because a number that stops supporting anything with no stated cause leaves the series unexplainable.
  • The number series is now keyed on the seller’s organisation number and the year, not on the site. A change of organisation number is almost always a different legal entity – a restructure, a sale of the business – and one counter per site would have carried one company’s series straight on into another’s, leaving neither able to show a complete record of its own invoiced sales. This had to be settled before the first document was issued: no migration can split a series that was already shared. Numbers now read 2026-000001.
  • The feature still ships switched off and cannot be switched on. What remains: the seller is typed by hand rather than resolved from the Surfboard merchant, a limited-company document is missing the “Foretaksregisteret” marking and a guaranteed head-office address, and reverse charge is not implemented.

4.0.714

  • The sales-document feature added in 4.0.711 now ships switched off and cannot be switched on. It is not finished, and three of the things it is missing cannot be repaired afterwards: there is no way to annul and replace a document that was issued with an error, the seller is typed by hand instead of resolved from the Surfboard merchant (so this plugin and Saga Regnskap could name two different sellers for the same sale), and a limited-company seller’s document is missing the word “Foretaksregisteret” and a guaranteed head-office address. A document number that has been used cannot be reused, so the series is not put into use until those are closed. The settings panel says exactly this instead of offering a checkbox.
  • Legal references corrected after checking them against the regulation rather than quoting from memory. The requirement that the document be sent to the buyer unprompted is bokf.forskr. § 5-2-1 fjerde ledd and is now quoted where the e-mail setting lives. The seller’s organisation number is § 5-1-1 nr. 2 jf. § 5-1-2 annet ledd. § 5-1-3 was cited for buyer identification, which it does not cover – it is about numbering and dating – and no longer is.

4.0.713

  • The Saga Payment Actions box now says what actually happened to a cancelled or failed order, instead of one fixed sentence. Found by opening real orders on a live shop: an order that had been authorised and then voided showed its payment id, “Status: VOIDED”, and “No payment was recorded for this order” – three statements on one screen contradicting each other. Such an order now reads “The authorisation on this order was released, so no money was collected”, a declined payment says it was declined, and a payment that was started and never finished says that. An order with no trace of a payment still says nothing was recorded.
  • The status line reads “Not completed” rather than “No payment recorded” when a payment id exists, so it no longer denies the id printed directly above it.
  • Every variant ends with the same sentence – “There is nothing to capture, void or refund” – because that is the part the merchant needs.

4.0.712

  • Fix (misleading, not dangerous): a cancelled or failed order showed “This payment method is auto-captured by the provider… use the WooCommerce Refund button instead” in the Saga Payment Actions box, on orders where no money had been taken at all. One condition was answering two different questions – it returns false both when the method genuinely auto-captures and when no payment method was ever recorded, which is the normal state of an order that failed before a payment existed – so “we do not know what this was” came out as a confident claim about how it was captured. Such an order now says plainly that no payment was recorded and there is nothing to capture, void or refund, and the status reads “No payment recorded” instead of “Unknown”.
  • The auto-capture notice is now only shown when the payment method is actually known; a redirect that the customer never completed no longer produces a claim about it.
  • An order that was cancelled while still holding an authorisation is unaffected: the funds are reserved, so the box still shows the Void button that releases them.

4.0.711

  • New: the shop can now issue the sales document itself. WooCommerce sends an order confirmation, but that carries an order number, not the sequential document number a sales document needs, so a shop without an invoice plugin has never actually issued one. Turn “Utsted salgsdokument” on under Saga Payments and every paid order gets a numbered PDF: it is attached to the customer’s order email, downloadable again from “min konto” and from the order page, shown on the admin order screen, and available to Saga Regnskap as the voucher itself.
  • Returns produce a credit note in the same number series, referencing the document it credits.
  • Invoices and credit notes share one continuous series so completeness is simple to verify. Numbers are allocated with a single atomic database statement, so two simultaneous orders can never receive the same one, and a number that gets allocated but never becomes a document is written to a void log rather than leaving an unexplained gap.
  • The document is frozen when issued: the PDF is rendered from a snapshot taken at that moment, so editing the order afterwards cannot rewrite a document the customer already holds.
  • If the shop already runs an invoice plugin, that plugin keeps its number series and Saga Payments passes its document through instead of issuing a competing one.
  • No document is issued until the organisation number is filled in, because a numbered document without it would not be valid – the settings panel says so rather than quietly issuing anyway.
  • The PDF is generated in-plugin with no third-party library, so this adds no new dependency to a payment gateway.
  • New for the accounting integration: GET /wp-json/saga-accounting/v1/orders/{id}/document and .../refunds/{id}/document return the document as application/pdf, and every order and refund in the list now carries document_number, document_type and document_url.

4.0.710

  • CRITICAL fix (a host that cannot take database locks would have taken payments and recorded none): the plugin asks the database for an advisory lock before completing a payment, and treated “no lock” as “another process is already completing this one” – which is correct when the lock is genuinely held by someone else, and catastrophic when the database simply cannot provide one. Connection multiplexers used by several managed hosts (AWS RDS Proxy, ProxySQL and similar) put that lock mechanism out of reach entirely. On such a host EVERY payment completion would have returned early: money taken at the payment system, no order marked paid, and nothing in the shop saying why. The two cases are now separated – real contention still backs off, an unavailable lock falls back to the completion marker the plugin has always kept on the order. On any normal host the behaviour is unchanged.
  • Same fix applied to the scheduled subscription renewal and the pre-order release, where yesterday’s lock hardening had introduced the same trap: on a host without lock support every renewal and every release would have been skipped on every pass, silently and forever. Both now fall back to the transient and order-meta guards that shipped before the lock existed.
  • The lock probe is a single cheap query per request with a zero-second timeout, so it can never itself wait on anything, and a database that refuses it is treated as “no locks available” rather than being allowed to break the site.

4.0.709

  • Hardening (the two automated charge paths were guarded by something that can vanish): the scheduled subscription renewal and the pre-order release each held a per-order lock stored as a WordPress transient. On a shop with a persistent object cache a transient lives in memory and is evicted under memory pressure or dropped entirely by any plugin that flushes the cache – so on a busy store the lock protecting an unattended charge could simply not be there. The safety net behind it does not close the gap on its own, because it reads the last-payment date, and that date is only written after the charge returns; two passes racing inside those few seconds both see “not charged recently”. Both paths now also take a MySQL advisory lock, which no cache can evict and which the database releases by itself if the PHP process dies mid-charge. The merchant-facing charge buttons have worked this way since 4.0.470 – it was the automated paths, the ones nobody is watching, that had only the transient.
  • The transient check is kept in front of it as a cheap first pass, and the last-charge safety window is kept behind it. Three layers, and only the middle one now depends on the object cache.
  • Every exit path releases the new lock, including a fatal mid-charge, so a double-charge guard cannot turn into a stuck subscription.

4.0.708

  • Fix (the “Charge Customer” button had the same half-guard the renewal button had): 4.0.706 taught it to recognise a charge whose confirmation timed out and stop calling that a failure – but it only recorded the payment system’s order id and nothing ever read it back. So the merchant was told to check before charging again, and if they pressed again anyway the plugin charged straight over a charge whose outcome nobody knew. Every path to a stored card now resolves an unconfirmed earlier charge with the payment system first: if it went through it is written into the books as its own order, if it definitively failed a fresh charge is allowed, and if nobody knows yet nothing is charged.
  • Fix: a manual charge discovered on reconciliation now creates the same WooCommerce order an immediate success would, with the same fee line, totals and payment id. Previously a charge that resolved late could have moved money with no order behind it – money the merchant could not see, refund or account for.
  • Internal: the amount of a submitted-but-unconfirmed manual charge is now recorded alongside its id, because the order that records it cannot be created without knowing how much moved.

4.0.707

  • Fix (a subscriber could be charged twice, found by driving a real renewal): 4.0.706 taught the “Test Renewal Now” button to recognise a charge that was submitted but never confirmed, park it, and tell the merchant “It will be reconciled automatically before any new attempt – the card will not be charged twice.” That promise was only true of the scheduled renewal. The button calls the payment API directly and never passes through the scheduled path, so the next press charged straight over the parked charge without ever asking what became of it. Measured on a live test subscription: one press parked an unconfirmed charge, the next press billed the card again. The reconciliation now lives in one place that every path to the card goes through, so the note and the behaviour agree.
  • Fix: the “force” checkbox on the same button skipped the double-charge safety net as well as the is-it-due check. A monthly subscription was measurably charged twice within four minutes. Force still overrides the due date – that is what it is for – but a subscription charged inside the same billing cycle is now refused whichever checkbox is ticked. To take a deliberate extra payment, use “Charge Customer”, which is an explicit ad-hoc charge with its own amount.
  • This was the first renewal ever charged against a real card in testing. Everything else about it held: one press produced exactly one renewal order, the renewal carried its own payment id distinct from the purchase, and an immediate second press was refused.

4.0.706

  • Fix (the same double-charge risk in the two places a merchant triggers a charge by hand): after fixing the pre-order release in 4.0.705, every remaining place that charges a card on file was audited for the same fault. Two more had it. The “charge saved card” button reported a plain “Payment failed” when the charge had in fact been submitted and only its confirmation had timed out – and a merchant who reads “failed” clicks it again. It now says the charge was submitted, names the Saga order so the merchant can check the portal, and tells them not to charge again until they have. The “test renewal” button routed the same case into the retry-scheduling failure handler, which is a retry on a card that may already have been debited; it now parks the charge for automatic reconciliation and schedules nothing.
  • All six places in the plugin that charge a stored card now treat “we did not get an answer” as its own outcome, never as a decline. A new test finds those places in the source rather than working from a list, so a seventh one cannot be added without the same handling.

4.0.705

  • Fix (a pre-order could charge the same card twice): releasing a pre-order charges a stored card with nobody watching. When the charge was submitted but its confirmation never arrived – a verification timeout, not a decline – the order was marked “failed” while the money may already have left the customer’s account. WooCommerce Pre-Orders lets a merchant retry a failed pre-order, and that retry was a second charge on top of one that had already gone through. A timeout is now recognised as its own outcome: the Saga order id is kept, the order goes on hold rather than failed, and the next release attempt asks the payment system what actually happened to that charge before the card is touched again. If it went through, it is recorded as the payment and nothing is charged; if its fate is still unknown, the release waits rather than gambling. The subscription renewal engine has worked this way since 4.0.471 – the pre-order release had never been given the same treatment.
  • Fix: a pre-order release could also be fired more than once – a double-clicked “complete”, a retried request, a batch run twice – and nothing checked whether the card had already been charged. An order already holding a completed payment is now never charged again, and two releases arriving together can no longer both get past that check.
  • No change for pre-orders that release normally, and a genuinely declined card still fails the order exactly as before.

4.0.704

  • Diagnostics: when the payment system refuses an order update, the reason it gave is now written to the debug log. The server has always sent it – which code, which error, which order – and the browser was throwing it away and logging only the sentence shown to the customer, so a real trace read “Could not update the order” and nothing about why. No change to what the customer sees.

4.0.703

  • Fix (4.0.702 could cancel an Apple Pay payment while the customer was still paying): yesterday’s fix released a stuck checkout after 25 seconds. A payment sheet is exactly where people take their time, and elapsed time cannot tell a dead wallet from an unhurried customer – so a customer standing in the Apple Pay sheet had their payment cancelled underneath them.
  • Nothing is cancelled automatically any more. If a wallet payment goes quiet, the customer is simply offered a way out – the same Avbryt betaling button Klarna has always shown – and they decide. Somebody mid-payment ignores it and finishes; somebody stuck clicks it and gets their checkout back. Only that click resets anything, and it still asks the server first, so a payment that went through a second earlier still completes.

4.0.702

  • Fix (an Apple Pay attempt could leave the checkout dead): if the Apple Pay sheet opened and then closed without the payment system reporting back, WooCommerce’s Place Order button kept spinning and the shopper could neither pay nor try again until they reloaded the page. Reported from a real attempt, where the payment system crashed inside its own cancel handler and so never told the shop anything. The checkout now releases the shopper on its own if a wallet payment goes quiet.
  • It still cannot tell a paying customer they have not paid: an Apple Pay or Google Pay sheet closes on success as well as on cancel, so the release asks the server whether the payment landed before concluding anything, and waits long enough for Face ID, a slow network and a late confirmation to finish first. A payment that did go through still completes normally.

4.0.701

  • SECURITY / DOUBLE-SHIPMENT FIX: one shopper’s payment could mark a different shopper’s order as paid. A shopper whose card had been declined, whose checkout tab was still open, could have their unpaid order completed by the next shopper’s payment – leaving two orders marked paid for one payment. A shop with automated fulfilment would ship two parcels and be paid for one. Reproduced end to end on the test store before the fix and refused after it.
  • The cause: several endpoints checked that the caller owned the ORDER but never that they owned the PAYMENT they named. Those are two different questions, and only one of them was being asked. Every endpoint that accepts both an order id and a payment id now verifies with the payment system that the payment belongs to that order before touching it – the same check the finalise endpoint has had since 4.0.493, now applied to all five of its siblings.
  • No change to normal payments: a shopper’s browser sends the payment id its own order already carries, which matches, and nothing extra is requested. The verification only runs when the two disagree.

4.0.700

  • The same guard added in 4.0.699 now also applies where the Blocks checkout reports which method was used. That value comes from the browser and lands in the payment method title that accounting exports group on, so it gets checked the same way as the one the payment system reports. No change to normal payments – this closes the other half of the same gap.

4.0.699

  • Fix (the order list showed “LOAD” instead of the payment method): the payment system reports the type of a transaction that is still open in the same field a finished payment reports its method in, and the plugin took it at face value. Two Klarna orders whose flow did not finish came out labelled LOAD – on one of them, where it should have said Klarna – and the same value went into the payment method title that accounting exports group on. Tokens that are not payment methods are now refused and logged, and what the order already knew is kept. A payment method the plugin has not heard of yet still displays as before, so a genuinely new one is not swallowed.
  • Fix (shoppers who never used a card were told to check their card details): every failed payment ended with “You have not been charged. Check your card details or choose another payment method.” A shopper refused by Klarna on credit has no card details to check, and the one thing they needed to read – choose another way to pay – sat behind advice they could not act on. The same applied to Vipps, Swish, MobilePay, Apple Pay and Google Pay. The wording now fits the method actually used, and an unknown method gets wording that is true whichever it was.

4.0.698

  • Fix (one payment was writing two “payment completed” lines in the order history): an ordinary card purchase confirms through more than one route – the SDK callback settles it, and the browser’s completion poll lands a moment later and settles it again – and each route wrote its own note. On two test purchases, one …