{"id":292686,"date":"2026-05-23T12:53:18","date_gmt":"2026-05-23T12:53:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/saga-payments-for-woocommerce\/"},"modified":"2026-09-17T20:05:27","modified_gmt":"2026-09-17T20:05:27","slug":"saga-payments","status":"publish","type":"plugin","link":"https:\/\/arg.wordpress.org\/plugins\/saga-payments\/","author":23469867,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"4.0.751","stable_tag":"4.0.751","tested":"7.1.1","requires":"5.8","requires_php":"7.4","requires_plugins":null,"header_name":"Saga Payments for WooCommerce","header_author":"Saga Payments","header_description":"Accept payments via Saga Payments - Card, Vipps, Klarna, Apple Pay, Google Pay, Swish, MobilePay and more.","assets_banners_color":"","last_updated":"2026-09-17 20:05:27","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/sagapay.no","rating":0,"author_block_rating":0,"active_installs":0,"downloads":1423,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"4.0.371":{"tag":"4.0.371","author":"sagapay","date":"2026-05-23 12:52:44","revision":3545124},"4.0.372":{"tag":"4.0.372","author":"sagapay","date":"2026-05-23 13:01:07","revision":3545138},"4.0.373":{"tag":"4.0.373","author":"sagapay","date":"2026-05-23 18:14:03","revision":3545498},"4.0.374":{"tag":"4.0.374","author":"sagapay","date":"2026-05-23 18:25:17","revision":3545517},"4.0.378":{"tag":"4.0.378","author":"sagapay","date":"2026-05-23 20:24:35","revision":3545600},"4.0.379":{"tag":"4.0.379","author":"sagapay","date":"2026-05-23 20:56:21","revision":3545613},"4.0.380":{"tag":"4.0.380","author":"sagapay","date":"2026-05-23 21:29:14","revision":3545626},"4.0.381":{"tag":"4.0.381","author":"sagapay","date":"2026-05-23 21:38:13","revision":3545634},"4.0.382":{"tag":"4.0.382","author":"sagapay","date":"2026-05-23 21:43:59","revision":3545637},"4.0.383":{"tag":"4.0.383","author":"sagapay","date":"2026-05-23 21:47:50","revision":3545641},"4.0.384":{"tag":"4.0.384","author":"sagapay","date":"2026-05-23 21:52:13","revision":3545644},"4.0.385":{"tag":"4.0.385","author":"sagapay","date":"2026-05-23 22:06:55","revision":3545654},"4.0.386":{"tag":"4.0.386","author":"sagapay","date":"2026-05-24 09:56:25","revision":3546057},"4.0.387":{"tag":"4.0.387","author":"sagapay","date":"2026-05-24 10:36:04","revision":3546081},"4.0.388":{"tag":"4.0.388","author":"sagapay","date":"2026-05-24 10:56:39","revision":3546127},"4.0.389":{"tag":"4.0.389","author":"sagapay","date":"2026-05-24 11:19:26","revision":3546162},"4.0.390":{"tag":"4.0.390","author":"sagapay","date":"2026-05-24 11:46:36","revision":3546201},"4.0.391":{"tag":"4.0.391","author":"sagapay","date":"2026-05-24 12:20:20","revision":3546224},"4.0.392":{"tag":"4.0.392","author":"sagapay","date":"2026-05-24 12:37:48","revision":3546239},"4.0.393":{"tag":"4.0.393","author":"sagapay","date":"2026-05-24 12:46:43","revision":3546251},"4.0.394":{"tag":"4.0.394","author":"sagapay","date":"2026-05-24 12:57:39","revision":3546264},"4.0.395":{"tag":"4.0.395","author":"sagapay","date":"2026-05-24 13:17:04","revision":3546276},"4.0.396":{"tag":"4.0.396","author":"sagapay","date":"2026-05-24 20:45:36","revision":3546597},"4.0.397":{"tag":"4.0.397","author":"sagapay","date":"2026-05-24 20:52:35","revision":3546600},"4.0.398":{"tag":"4.0.398","author":"sagapay","date":"2026-05-24 21:16:23","revision":3546621},"4.0.399":{"tag":"4.0.399","author":"sagapay","date":"2026-05-24 21:32:25","revision":3546627},"4.0.400":{"tag":"4.0.400","author":"sagapay","date":"2026-05-24 21:45:47","revision":3546639},"4.0.401":{"tag":"4.0.401","author":"sagapay","date":"2026-05-24 21:56:23","revision":3546652},"4.0.402":{"tag":"4.0.402","author":"sagapay","date":"2026-05-24 22:13:42","revision":3546672},"4.0.403":{"tag":"4.0.403","author":"sagapay","date":"2026-05-24 22:16:11","revision":3546677},"4.0.404":{"tag":"4.0.404","author":"sagapay","date":"2026-05-24 22:20:13","revision":3546684},"4.0.405":{"tag":"4.0.405","author":"sagapay","date":"2026-05-24 22:38:23","revision":3546700},"4.0.406":{"tag":"4.0.406","author":"sagapay","date":"2026-05-25 09:48:32","revision":3547276},"4.0.407":{"tag":"4.0.407","author":"sagapay","date":"2026-05-25 10:16:30","revision":3547337},"4.0.408":{"tag":"4.0.408","author":"sagapay","date":"2026-05-25 10:26:38","revision":3547367},"4.0.409":{"tag":"4.0.409","author":"sagapay","date":"2026-05-25 10:33:56","revision":3547381},"4.0.410":{"tag":"4.0.410","author":"sagapay","date":"2026-05-25 10:37:27","revision":3547390},"4.0.411":{"tag":"4.0.411","author":"sagapay","date":"2026-05-25 11:11:35","revision":3547435},"4.0.412":{"tag":"4.0.412","author":"sagapay","date":"2026-05-25 11:21:58","revision":3547463},"4.0.420":{"tag":"4.0.420","author":"sagapay","date":"2026-05-27 08:56:04","revision":3550345},"4.0.445":{"tag":"4.0.445","author":"sagapay","date":"2026-06-09 13:35:33","revision":3566073},"4.0.446":{"tag":"4.0.446","author":"sagapay","date":"2026-06-10 17:04:52","revision":3567861},"4.0.447":{"tag":"4.0.447","author":"sagapay","date":"2026-06-10 19:26:56","revision":3567998},"4.0.448":{"tag":"4.0.448","author":"sagapay","date":"2026-06-18 17:29:22","revision":3577619},"4.0.449":{"tag":"4.0.449","author":"sagapay","date":"2026-06-19 17:19:18","revision":3579016},"4.0.451":{"tag":"4.0.451","author":"sagapay","date":"2026-06-25 12:11:41","revision":3586142},"4.0.452":{"tag":"4.0.452","author":"sagapay","date":"2026-06-30 11:49:08","revision":3591433},"4.0.453":{"tag":"4.0.453","author":"sagapay","date":"2026-06-30 12:14:25","revision":3591472},"4.0.454":{"tag":"4.0.454","author":"sagapay","date":"2026-06-30 12:22:56","revision":3591486},"4.0.455":{"tag":"4.0.455","author":"sagapay","date":"2026-06-30 13:26:02","revision":3591564},"4.0.456":{"tag":"4.0.456","author":"sagapay","date":"2026-06-30 13:41:39","revision":3591579},"4.0.457":{"tag":"4.0.457","author":"sagapay","date":"2026-06-30 14:08:57","revision":3591615},"4.0.458":{"tag":"4.0.458","author":"sagapay","date":"2026-07-04 21:09:58","revision":3596199},"4.0.459":{"tag":"4.0.459","author":"sagapay","date":"2026-07-04 21:22:21","revision":3596204},"4.0.460":{"tag":"4.0.460","author":"sagapay","date":"2026-07-05 09:30:44","revision":3596577},"4.0.461":{"tag":"4.0.461","author":"sagapay","date":"2026-07-05 10:20:55","revision":3596622},"4.0.462":{"tag":"4.0.462","author":"sagapay","date":"2026-07-05 11:00:51","revision":3596661},"4.0.463":{"tag":"4.0.463","author":"sagapay","date":"2026-07-06 09:15:39","revision":3597482},"4.0.464":{"tag":"4.0.464","author":"sagapay","date":"2026-07-10 10:34:17","revision":3602582},"4.0.465":{"tag":"4.0.465","author":"sagapay","date":"2026-07-10 12:11:15","revision":3602773},"4.0.466":{"tag":"4.0.466","author":"sagapay","date":"2026-07-10 13:38:51","revision":3602924},"4.0.467":{"tag":"4.0.467","author":"sagapay","date":"2026-07-15 14:44:36","revision":3609039},"4.0.468":{"tag":"4.0.468","author":"sagapay","date":"2026-07-18 10:15:35","revision":3612499},"4.0.710":{"tag":"4.0.710","author":"sagapay","date":"2026-08-15 19:58:47","revision":3649000},"4.0.714":{"tag":"4.0.714","author":"sagapay","date":"2026-08-31 11:39:38","revision":3674023},"4.0.718":{"tag":"4.0.718","author":"sagapay","date":"2026-08-31 12:12:10","revision":3674143},"4.0.724":{"tag":"4.0.724","author":"sagapay","date":"2026-09-06 21:50:41","revision":3683977},"4.0.746":{"tag":"4.0.746","author":"sagapay","date":"2026-09-17 14:52:37","revision":3700470},"4.0.747":{"tag":"4.0.747","author":"sagapay","date":"2026-09-17 15:31:20","revision":3700527},"4.0.749":{"tag":"4.0.749","author":"sagapay","date":"2026-09-17 17:08:00","revision":3700657},"4.0.750":{"tag":"4.0.750","author":"sagapay","date":"2026-09-17 18:00:25","revision":3700761},"4.0.751":{"tag":"4.0.751","author":"sagapay","date":"2026-09-17 20:05:27","revision":3700922}},"upgrade_notice":{"1.0.0":"<p>Initial release of Saga Payments for WooCommerce.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3545137,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3545137,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["4.0.371","4.0.372","4.0.373","4.0.374","4.0.378","4.0.379","4.0.380","4.0.381","4.0.382","4.0.383","4.0.384","4.0.385","4.0.386","4.0.387","4.0.388","4.0.389","4.0.390","4.0.391","4.0.392","4.0.393","4.0.394","4.0.395","4.0.396","4.0.397","4.0.398","4.0.399","4.0.400","4.0.401","4.0.402","4.0.403","4.0.404","4.0.405","4.0.406","4.0.407","4.0.408","4.0.409","4.0.410","4.0.411","4.0.412","4.0.420","4.0.445","4.0.446","4.0.447","4.0.448","4.0.449","4.0.451","4.0.452","4.0.453","4.0.454","4.0.455","4.0.456","4.0.457","4.0.458","4.0.459","4.0.460","4.0.461","4.0.462","4.0.463","4.0.464","4.0.465","4.0.466","4.0.467","4.0.468","4.0.710","4.0.714","4.0.718","4.0.724","4.0.746","4.0.747","4.0.749","4.0.750","4.0.751"],"block_files":[],"assets_screenshots":[],"screenshots":{"1":"Checkout page with inline payment form showing card, Vipps, Klarna, Apple Pay, Google Pay, Swish, and MobilePay","2":"WooCommerce payment gateway settings with easy configuration","3":"Order management with capture, void, and refund controls"}},"plugin_section":[],"plugin_tags":[150561,223971,6593,158460,286],"plugin_category":[45],"plugin_contributors":[264123],"plugin_business_model":[],"class_list":["post-292686","plugin","type-plugin","status-publish","hentry","plugin_tags-klarna","plugin_tags-mobilepay","plugin_tags-payment-gateway","plugin_tags-vipps","plugin_tags-woocommerce","plugin_category-ecommerce","plugin_contributors-sagapay","plugin_committers-sagapay"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/saga-payments\/assets\/icon-128x128.png?rev=3545137","icon_2x":"https:\/\/ps.w.org\/saga-payments\/assets\/icon-256x256.png?rev=3545137","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Saga Payments for WooCommerce is a <strong>full-featured payment gateway<\/strong> built for Nordic merchants. Accept secure payments through multiple payment methods with enterprise-grade features:<\/p>\n\n<h4>Payment Methods<\/h4>\n\n<ul>\n<li><strong>Card Payments<\/strong> - Visa, Mastercard, American Express, Discover<\/li>\n<li><strong>Vipps<\/strong> - Norway's most popular mobile payment app<\/li>\n<li><strong>Klarna<\/strong> - Buy now, pay later<\/li>\n<li><strong>Apple Pay<\/strong> - Fast checkout for Apple users<\/li>\n<li><strong>Google Pay<\/strong> - Fast checkout for Android users<\/li>\n<li><strong>Swish<\/strong> - Sweden's most popular mobile payment app<\/li>\n<li><strong>MobilePay<\/strong> - Denmark and Finland's popular mobile payment app<\/li>\n<\/ul>\n\n<h4>Features<\/h4>\n\n<ul>\n<li><strong>Easy Setup<\/strong> - Just enter your Merchant ID, Terminal ID, and Public Key<\/li>\n<li><strong>Saved Payment Methods<\/strong> - Customers can save cards for faster checkout<\/li>\n<li><strong>WooCommerce Subscriptions<\/strong> - Full support for recurring payments with automatic renewals<\/li>\n<li><strong>WooCommerce Pre-Orders<\/strong> - Charge customers when pre-ordered products become available<\/li>\n<li><strong>Authorize &amp; Capture<\/strong> - Authorize payments and capture later, or capture immediately<\/li>\n<li><strong>Auto-Capture<\/strong> - Automatically capture when order status changes to Processing\/Completed<\/li>\n<li><strong>Auto-Void<\/strong> - Automatically void authorizations when orders are cancelled<\/li>\n<li><strong>Express Checkout<\/strong> - Apple Pay\/Google Pay buttons on product pages and cart<\/li>\n<li><strong>WooCommerce Blocks<\/strong> - Full support for Blocks Checkout including saved cards<\/li>\n<li><strong>HPOS Compatible<\/strong> - Full support for High-Performance Order Storage<\/li>\n<li><strong>Refunds<\/strong> - Full and partial refunds directly from WooCommerce admin<\/li>\n<li><strong>Professional Design<\/strong> - Clean, responsive payment widget<\/li>\n<\/ul>\n\n<h4>Plugin Integrations<\/h4>\n\n<ul>\n<li>WooCommerce Subscriptions - Automatic recurring payments<\/li>\n<li>WooCommerce Pre-Orders - Charge on release<\/li>\n<li>WooCommerce Blocks - Full Blocks checkout support<\/li>\n<li>WooCommerce HPOS - High-Performance Order Storage<\/li>\n<li><strong>Saga Product Sync<\/strong> - Bidirectional product catalogue synchronisation<\/li>\n<li>Compatible with all major WordPress themes<\/li>\n<\/ul>\n\n<h4>Product Sync (Saga)<\/h4>\n\n<p>Automatic bidirectional synchronisation between your WooCommerce store and Saga:<\/p>\n\n<ul>\n<li><strong>Saga is master<\/strong> - pull products, prices, images, VAT rates and stock from Saga into WooCommerce<\/li>\n<li><strong>Push changes back<\/strong> - WooCommerce product edits are automatically pushed to Saga in real-time<\/li>\n<li><strong>Inventory sync<\/strong> - relative stock deltas (STOCK_DOWN \/ STOCK_UP) on order completion, refund and cancellation<\/li>\n<li><strong>Auto-retry<\/strong> - failed inventory adjustments are queued and retried every 5 minutes (up to 20 attempts)<\/li>\n<li><strong>Full field mapping<\/strong> - prices (\u00c3\u00b8re  kroner), VAT rates (25 \/ 15 \/ 12 \/ 0 %), units, barcodes, cost price, images<\/li>\n<li><strong>Admin panel<\/strong> - WooCommerce -&gt; Saga Product Sync with connection test, manual sync, and status dashboard<\/li>\n<li>Configure via WooCommerce -&gt; Saga Product Sync<\/li>\n<\/ul>\n\n<h4>Requirements<\/h4>\n\n<ul>\n<li>WordPress 5.8 or later<\/li>\n<li>WooCommerce 7.0 or later<\/li>\n<li>PHP 7.4 or later<\/li>\n<li>SSL certificate (HTTPS)<\/li>\n<li>Saga Payments merchant account<\/li>\n<\/ul>\n\n<h3>External services<\/h3>\n\n<p>This plugin relies on the following external services to process payments. A Saga Payments merchant account is required.<\/p>\n\n<h4>Saga Payments API<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Service URL: https:\/\/sagapay-api-v3.kristoffer-afc.workers.dev\/api<\/li>\n<li>Provider: Saga Payments (Cloudflare Worker proxy to Surfboard Payments)<\/li>\n<li>Website: https:\/\/sagapay.no<\/li>\n<li>Terms of Service: https:\/\/www.sagapay.no\/terms-of-service<\/li>\n<li>Privacy Policy: https:\/\/www.sagapay.no\/personvernserklaering<\/li>\n<\/ul>\n\n<h4>Saga Payments Merchant Dashboard and Documentation<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Dashboard URL: https:\/\/dashboard.sagapay.no<\/li>\n<li>API key documentation URL: https:\/\/docs.sagapay.no\/guides\/getting-started\/opprett-api-nokler<\/li>\n<li>Webhook documentation URL: https:\/\/docs.sagapay.no\/guides\/getting-started\/webhooks<\/li>\n<li>Product catalogue documentation URL: https:\/\/docs.sagapay.no\/api-reference\/api-reference\/product-catalogue\/<\/li>\n<li>Provider: Saga Payments<\/li>\n<li>Terms of Service: https:\/\/www.sagapay.no\/terms-of-service<\/li>\n<li>Privacy Policy: https:\/\/www.sagapay.no\/personvernserklaering<\/li>\n<\/ul>\n\n<h4>Saga OTP Verification Service<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Service URL: https:\/\/saga-otp-worker.kristoffer-afc.workers.dev<\/li>\n<li>Provider: Saga Payments (Cloudflare Worker)<\/li>\n<li>Website: https:\/\/sagapay.no<\/li>\n<li>Terms of Service: https:\/\/www.sagapay.no\/terms-of-service<\/li>\n<li>Privacy Policy: https:\/\/www.sagapay.no\/personvernserklaering<\/li>\n<\/ul>\n\n<h4>Saga Regnskap (Accounting) Integration<\/h4>\n\n<p>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 \u00e2\u20ac\u201d 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.<\/p>\n\n<ul>\n<li>Service URL: https:\/\/regnskap.sagapay.no (fallback: https:\/\/saga-regnskap-api.kristoffer-afc.workers.dev)<\/li>\n<li>Provider: Saga Payments (Cloudflare Worker)<\/li>\n<li>Website: https:\/\/sagapay.no<\/li>\n<li>Terms of Service: https:\/\/www.sagapay.no\/terms-of-service<\/li>\n<li>Privacy Policy: https:\/\/www.sagapay.no\/personvernserklaering<\/li>\n<\/ul>\n\n<h4>Surfboard Online SDK<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>SDK URL: https:\/\/thorium.surfgw.com\/OnlineSDK.js<\/li>\n<li>Provider: Surfboard Payments<\/li>\n<li>Website: https:\/\/www.surfboardpayments.com<\/li>\n<li>Terms of Service: https:\/\/www.surfboardpayments.com\/terms-and-conditions<\/li>\n<li>Privacy Policy: https:\/\/www.surfboardpayments.com\/privacy-policy<\/li>\n<\/ul>\n\n<h4>Surfboard Hosted Payment Page<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Service URL: https:\/\/pay.withsurfboard.com<\/li>\n<li>Base URL used by redirects: https:\/\/pay.withsurfboard.com\/<\/li>\n<li>Provider: Surfboard Payments<\/li>\n<li>Terms of Service: https:\/\/www.surfboardpayments.com\/terms-and-conditions<\/li>\n<li>Privacy Policy: https:\/\/www.surfboardpayments.com\/privacy-policy<\/li>\n<\/ul>\n\n<h4>Surfboard Vipps\/MobilePay Intermediary<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Service URL: https:\/\/vipps.withsurfboard.com<\/li>\n<li>Provider: Surfboard Payments<\/li>\n<li>Terms of Service: https:\/\/www.surfboardpayments.com\/terms-and-conditions<\/li>\n<li>Privacy Policy: https:\/\/www.surfboardpayments.com\/privacy-policy<\/li>\n<\/ul>\n\n<h4>Google Pay API<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>SDK URL: https:\/\/pay.google.com\/gp\/p\/js\/pay.js<\/li>\n<li>Provider: Google<\/li>\n<li>Website: https:\/\/pay.google.com<\/li>\n<li>Terms of Service: https:\/\/payments.google.com\/payments\/apis-secure\/get_legal_document?ldo=0&amp;ldt=googlepaytos<\/li>\n<li>Privacy Policy: https:\/\/policies.google.com\/privacy<\/li>\n<\/ul>\n\n<h4>QR Code Generation<\/h4>\n\n<p>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.<\/p>\n\n<h4>Saga Checkout SDK (Diagnostics Only)<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Provider: Saga Payments<\/li>\n<li>Website: https:\/\/sagapay.no<\/li>\n<li>Terms of Service: https:\/\/www.sagapay.no\/terms-of-service<\/li>\n<li>Privacy Policy: https:\/\/www.sagapay.no\/personvernserklaering<\/li>\n<\/ul>\n\n<h4>Saga Product Sync API<\/h4>\n\n<p>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.<\/p>\n\n<ul>\n<li>Service URL: https:\/\/api.sagapay.no<\/li>\n<li>Provider: Saga Payments<\/li>\n<li>Terms of Service: https:\/\/www.sagapay.no\/terms-of-service<\/li>\n<li>Privacy Policy: https:\/\/www.sagapay.no\/personvernserklaering<\/li>\n<\/ul>\n\n<h4>Payment Method Logos<\/h4>\n\n<p>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.\nSVG 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.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>saga-payments<\/code> folder to the <code>\/wp-content\/plugins\/<\/code> directory<\/li>\n<li>Activate the plugin through the 'Plugins' menu in WordPress<\/li>\n<li>Go to WooCommerce &gt; Settings &gt; Payments &gt; Saga Payments<\/li>\n<li>Enter your <strong>Merchant ID<\/strong> and <strong>Store ID<\/strong> (from your Saga Payments dashboard at https:\/\/dashboard.sagapay.no) and click <strong>Save changes<\/strong><\/li>\n<li>The plugin will automatically provision your <strong>Terminal ID<\/strong> and <strong>Public Key<\/strong> on save - no manual key handling required<\/li>\n<li>Enable the payment method and save<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20do%20i%20get%20a%20saga%20payments%20account%3F\"><h3>How do I get a Saga Payments account?<\/h3><\/dt>\n<dd><p>Contact Saga Payments at support@sagapayments.com to set up your merchant account.<\/p><\/dd>\n<dt id=\"does%20this%20support%20test%20mode%3F\"><h3>Does this support test mode?<\/h3><\/dt>\n<dd><p>Yes! Enable test mode in the plugin settings to test without processing real payments.<\/p><\/dd>\n<dt id=\"which%20currencies%20are%20supported%3F\"><h3>Which currencies are supported?<\/h3><\/dt>\n<dd><p>The plugin supports NOK (Norwegian Krone), SEK (Swedish Krona), DKK (Danish Krone), EUR and other currencies supported by Saga Payments.<\/p><\/dd>\n<dt id=\"can%20customers%20save%20their%20cards%3F\"><h3>Can customers save their cards?<\/h3><\/dt>\n<dd><p>Yes! When \"Enable Saved Cards\" is turned on in settings, logged-in customers can save their cards for faster future checkout.<\/p><\/dd>\n<dt id=\"does%20this%20work%20with%20woocommerce%20subscriptions%3F\"><h3>Does this work with WooCommerce Subscriptions?<\/h3><\/dt>\n<dd><p>Yes! Full integration with WooCommerce Subscriptions for automatic recurring payments, payment method changes, and subscription lifecycle management.<\/p><\/dd>\n<dt id=\"can%20i%20authorize%20and%20capture%20later%3F\"><h3>Can I authorize and capture later?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>4.0.751<\/h4>\n\n<ul>\n<li>Product sync: a WooCommerce sale is no longer frozen into the catalogue product. Since 4.0.556 the sale price was also written into productProperties (_campaignPrice \/ _campaignActive \/ salePrice) for the till to charge \u2014 but those properties are create-only upstream, so a product created while on sale kept the sale price in the catalogue after the sale had ended in the shop (measured 2026-09-17), and a till reading that field would keep charging it. The sale now travels only as the catalogue's campaign entity (created and deleted with the sale since 4.0.748), which the till is being updated to read.<\/li>\n<\/ul>\n\n<h4>4.0.750<\/h4>\n\n<ul>\n<li>Product sync: a SKU set at the till reaches WooCommerce. SagaPOS stores a product SKU of exactly 4, 6 or 8 digits as the catalogue's hsnCode and any other SKU as productProperties._sku; the pull read neither, so every product created on the till arrived in WooCommerce without a SKU. Both are read now (confirmed with the SagaPOS team). A variation's SKU is read from variantProperties._sku; a variation's hsnCode is the till's HSN column and is never taken as a SKU.<\/li>\n<li>Product sync: a SKU set in WooCommerce reaches the till by the same rule \u2014 4\/6\/8 digits as hsnCode (unless a real HSN code is set on the product, in which case the SKU travels as productProperties._sku), anything else as productProperties._sku \u2014 so the register can show it. A variation's SKU is sent as variantProperties._sku.<\/li>\n<\/ul>\n\n<h4>4.0.749<\/h4>\n\n<ul>\n<li>Product sync: a variation deleted in WooCommerce now leaves the till. The catalogue has no DELETE on \u2026\/variants\/{id}; a variant is removed by DELETE \/catalog\/{catalogue}\/products\/{variantId} \u2014 the variant id in the product slot, storeId as a query parameter. Measured with the Saga API team. The same call with the parent id would delete the whole product, so the plugin refuses to send it unless the id is a stored variant id that differs from the parent's.<\/li>\n<li>Product sync: a variation's barcode reaches the till. The catalogue drops the barcode on variant create in either casing and only takes it via a PATCH afterwards; the plugin now sends that PATCH right after the create.<\/li>\n<li>Product sync: the product barcode is sent under the lowercase key only \u2014 a camel-cased key in an update is rejected by the catalogue (PC_0038).<\/li>\n<li>Product sync: a product deleted at the till (or in the catalogue) no longer comes back from WooCommerce under a new id. The push's link check unpinned every linked product whose id the catalogue list no longer held and re-created it \u2014 measured on panta.no: a product deleted upstream at 15:57 was back at 16:00:54 with a new id, while the orphan check was correctly waiting out its grace. The link check now only unpins products pinned to ANOTHER catalogue (a moved store); a product pinned to this catalogue and gone from it is left to the orphan check, which retires the shop's copy \u2014 the till's deletion wins, and restoring it from the trash re-creates it. Links without a catalogue stamp are not judged within 15 minutes of their last push (the list does not always show a product created seconds ago).<\/li>\n<\/ul>\n\n<h4>4.0.748<\/h4>\n\n<ul>\n<li>Product sync: a WooCommerce sale now reaches the till. The campaign call used a portal-style path (\u2026\/product-catalogue\/\u2026\/product\/\u2026\/product-campaign) that the catalogue API answers with 404; the route is \/catalog\/{catalogue}\/products\/{product}\/campaign with the campaign name and a fixed amount in minor units. Verified against the API together with the Saga API team.<\/li>\n<li>Product sync: the product barcode\/GTIN now reaches the till. The catalogue stores it under <code>barcode<\/code> (lowercase); the plugin sent <code>barCode<\/code>, which the catalogue accepted and silently dropped, so every GTIN came back empty.<\/li>\n<\/ul>\n\n<h4>4.0.747<\/h4>\n\n<ul>\n<li>Product sync: a product without a SKU is recognised by its name when pushed, the way the pull has always recognised it. The push's duplicate check was SKU-only, so a SagaPOS-born product with no SKU that also existed in WooCommerce was pushed as a NEW catalogue product \u2014 the till showed it twice, and the shop was linked to the copy. Measured: thirteen such copies on the test store, now healed. Exactly one catalogue product with the same name that nobody else owns is linked; an ambiguous name is not guessed.<\/li>\n<\/ul>\n\n<h4>4.0.746<\/h4>\n\n<ul>\n<li>Product sync: a product the shop put in the trash does not come back on its own. Every WooCommerce lookup the pull used to recognise a product is blind to the trash, so a catalogue row whose shop product had been trashed (the withdrawal failed, or the row was a second copy) matched nothing and was created again as a new product on the next pull \u2014 measured on 2026-09-10, two products trashed at 11:35 were back, published, at 11:36. A trashed product with the same SKU, or the same exact name, now stops the create; the log says which product and what to do (restore it in WooCommerce, or delete the row in SagaPOS).<\/li>\n<\/ul>\n\n<h4>4.0.745<\/h4>\n\n<ul>\n<li>Product sync: a variation's price now goes up through the tax lens like every other price. The variant payload was built outside the mapper and skipped the lens, so for a shop storing prices without tax the pull read a variant through the lens while the push sent it bare \u2014 the two disagreed by the rate on every run (measured on the live tax drive).<\/li>\n<\/ul>\n\n<h4>4.0.744<\/h4>\n\n<ul>\n<li>Product sync: the catalogue's VAT is the shop's own echo when it is what the push sent. A product pushed in a tax class the nominal map does not know (a Norwegian shop's <code>redusert-sats<\/code>, say) while that class had no rate went up with \"VAT 25\"; the first pull read 25, had never seen a rate on the product, called that a change made at the till and set the class to standard \u2014 the merchant's class gone, silently, and every later pull priced the product at the wrong rate. A rate that equals what the push would send for the class the product has is not a change and is not a mismatch; only a rate that differs from that, and from the last one seen, reaches the shop.<\/li>\n<\/ul>\n\n<h4>4.0.743<\/h4>\n\n<ul>\n<li>Product sync: a variable product that manages one stock count for all its variations sent that count up once PER VARIANT (measured: a parent with 7 units and two variations put 14 on the till), and the pull then switched each variation to its own stock management with the parent's number (the shop would have sold 21). The parent's count now goes up once, on the product; a parent-managed variation is left as the merchant set it.<\/li>\n<\/ul>\n\n<h4>4.0.742<\/h4>\n\n<ul>\n<li>Product sync: the tax lens on the pull is the class the product HAS in the shop, not the class the payload's nominal VAT maps to. Measured on the live tax drive: a product in the reduced class was pushed at 80 \u00d7 1,15 = 92,00 and pulled back as 92,00, because the payload said VAT 25, the map said standard, and standard had no rate.<\/li>\n<li>Product sync: the till is told the VAT the shop really charges for the product's class (the sum of its base rates), and a VAT from the catalogue resolves to the shop's own class with that rate before the nominal map (a Norwegian shop's <code>redusert-sats<\/code> at 15 %, not the English slug that does not exist there).<\/li>\n<\/ul>\n\n<h4>4.0.741<\/h4>\n\n<ul>\n<li>Product sync: no single-product push while a sync runs in another process. Three times on 2026-09-07 a product got two catalogue entries: the deferred push created it while \"Synk n\u00e5\" was pulling, and that request's create loop, half a minute later, still did not see the link \u2014 through a purged cache and through a direct database read alike. A deferred or admin-save push that finds the pull or push lock held by another process now waits 90 seconds instead; by then the sync has either created the product (the push finds the link and updates) or finished without it (the push creates, alone).<\/li>\n<\/ul>\n\n<h4>4.0.740<\/h4>\n\n<ul>\n<li>Product sync: the SKU index is rebuilt for every push run, so a product another process created seconds earlier is found by its SKU instead of being created again.<\/li>\n<li>Product sync: right before any create, the log now records what every source says about the product's link (the loaded object, the database, a fresh meta read, the row count, the connection's isolation level and the SKU index age) \u2014 a second catalogue product was still measured on 4.0.739, and the next occurrence must be a measurement, not a theory.<\/li>\n<\/ul>\n\n<h4>4.0.739<\/h4>\n\n<ul>\n<li>Product sync: the database is the only re-read. 4.0.736 purged WordPress's post-meta cache before re-checking a product's link; measured on 4.0.738, a second catalogue product was still created for a product the deferred push had linked 25 seconds earlier \u2014 WooCommerce keeps its own per-object meta cache, and WordPress caches query results per request, so the \"re-read\" was this request's stale copy on two more levels. Both create paths now ask the database directly right before creating, and the unlinked-products query is never answered from the request's query cache.<\/li>\n<\/ul>\n\n<h4>4.0.738<\/h4>\n\n<ul>\n<li>Product sync: an empty barcode is no barcode. The catalogue answers <code>\"barCode\":\"\"<\/code> for a product it holds no barcode for; the pull took that as a barcode and emptied the merchant's GTIN\/EAN (<code>global_unique_id<\/code>, <code>_barcode<\/code>, <code>_ean<\/code>, <code>_gtin<\/code>) on every pull. Only a non-empty value is a barcode now.<\/li>\n<li>Product sync: the till cannot count below zero. A product sold on backorder (negative stock in WooCommerce) was set to 0 and marked OUT OF STOCK by the pull, and the web stopped selling it. The shop's negative is kept when the till reports 0, and every stock status now follows WooCommerce's own rule ('onbackorder' when backorders are allowed).<\/li>\n<li>Product sync: the push-side SKU dedupe never read <code>productProperties.wcSku<\/code> \u2014 the field this plugin itself writes \u2014 so it was blind to every product it had created, and a product that lost its link (a repaired shared link, for example) was given a second catalogue product. The index now reads it, and a product whose stored payload names a catalogue product that still exists and has no other owner is relinked to it instead.<\/li>\n<\/ul>\n\n<h4>4.0.737<\/h4>\n\n<ul>\n<li>Product sync: the catalogue may still hold the figure an earlier build pushed without the tax lens (4.0.736). For a shop that stores prices without tax, the first pull after upgrading would have read that figure through the lens and cut every shop price by a fifth. A catalogue price that equals the shop's stored price to the \u00f8re, while the lens applies, is now recognised as the shop's own echo: the price is kept and sent up again through the lens.<\/li>\n<li>Product sync: variation prices go through the tax lens like the parent's, on both the push and the pull.<\/li>\n<li>Product sync: a variation's <code>salePrice<\/code> in the catalogue is what the shop itself wrote when a sale was pushed, and cannot be cleared by a later push. Reading it back re-applied a sale the merchant had ended (the defect 4.0.732 fixed for simple products). Only the till's own discount is now a sale on a variation; a sale the merchant set is left alone; a sale that came from the till is cleared when the till drops it.<\/li>\n<\/ul>\n\n<h4>4.0.736<\/h4>\n\n<ul>\n<li>Product sync: one catalogue product per push. When a product saved outside wp-admin was carried up by the deferred push while \"Synk n\u00e5\" was running, the running push created the same product AGAIN \u2014 it re-read the link from its own memory, which another process cannot update. Measured: three products doubled in one run. Both push paths now re-read the link from the database after taking the lock.<\/li>\n<li>Product sync: a catalogue row whose SKU already belongs to a product linked to another catalogue id is a duplicate upstream, not a new product. The pull used to refuse the relink and then create a copy in the shop anyway; it now reports the duplicate and creates nothing.<\/li>\n<li>Product sync: a name longer than 255 characters is fitted on the way out, so the catalogue's copy never matched the shop's title and the duplicate guard could not see it. Fitted names are now matched by prefix and compared as the push would send them.<\/li>\n<li>Product sync: the deferred push and the five-minute cron could fall due in the same cron request and send the same body twice. Whichever runs second now sees the product was pushed after its last edit and sends nothing.<\/li>\n<li>Product sync: a linked external or grouped product is taken off the till by the pull, whether or not it was edited since the last push (4.0.734 only did so for edited products).<\/li>\n<li>Product sync: the till's price is what the customer pays. A shop that stores prices WITHOUT tax and adds it at the checkout (WooCommerce's default) was sending the bare figure to the till \u2014 20 % under the web at 25 % VAT \u2014 and the pull wrote the till's consumer price into the ex-VAT field, 25 % over. Prices now pass through the shop's real base tax rates both ways; shops storing prices including tax, and shops with no rates, are untouched.<\/li>\n<\/ul>\n\n<h4>4.0.735<\/h4>\n\n<ul>\n<li>Product sync: \"Sync now\" says when it did not run. A second click while a sync was already running was answered \"Synk fullf\u00f8rt \u2014 0 opprettet, 0 oppdatert \u2026\", which reads as \"it ran and found nothing\"; it had been refused by the lock. It now says that a sync is already running and to try again shortly.<\/li>\n<\/ul>\n\n<h4>4.0.734<\/h4>\n\n<ul>\n<li>Product sync: a product that does not manage stock in WooCommerce - a service, a virtual or downloadable item, anything the merchant left untracked - is no longer forced out of stock by the pull. The catalogue's count for such a product is 0, and the pull turned stock management on and wrote that 0, so the product could not be bought (measured on the test shop: a virtual product was out of stock two minutes after it was created). Stock now follows the till only where the shop tracks it.<\/li>\n<li>Product sync: external and grouped products are no longer sent to the till. An external product is sold on another site and a grouped product has no price of its own; both were pushed as ordinary catalogue products (a partner's item and a 0 kr phantom on the till). They are skipped, and any that an earlier build pushed are withdrawn.<\/li>\n<li>Product sync: a product name longer than the catalogue's 255-character limit is fitted on a word boundary instead of being refused every five minutes (\"Length validation failed\"). The shop keeps its full name; the pull recognises the fitted copy as its own.<\/li>\n<li>Product sync: data the catalogue refuses outright - a price above 999 999,99 kr, for instance (\"Expecting values between 0-99999999\") - is no longer re-sent every five minutes. It is left alone for an hour, or until the merchant edits the product, and the reason stays on the sync page.<\/li>\n<li>Product sync: a product the sync had retired (its catalogue entry gone) and the merchant then restored from the trash is no longer retired again by the next pull. The dead catalogue link and the retirement stamps are dropped on restore, and the next push creates it in the catalogue afresh.<\/li>\n<li>Product sync: a SKU that already belongs to another shop product's catalogue entry is no longer linked to it. Two shop products on one catalogue product would overwrite each other upstream and pull each other's data; the newcomer now gets its own entry. A product in the trash yields its entry to the newcomer.<\/li>\n<\/ul>\n\n<h4>4.0.733<\/h4>\n\n<ul>\n<li>Product sync: a variation deleted in WooCommerce no longer comes back. Deleting a variation told the catalogue nothing, and the next pull - finding a catalogue variant with no variation behind it - created it again: measured on the test shop, a variation deleted at 10:07 was back at 10:09 with the same catalogue id. The deletion is now remembered on the product, so the pull never re-creates that variant whatever the catalogue lists, and it is withdrawn from the catalogue too, with the catalogue's answer in the log if it refuses.<\/li>\n<li>Product sync: a sale the merchant ended in WooCommerce no longer comes back from the catalogue. The plugin writes the sale price into the catalogue's product properties when a product is created, and those properties never change afterwards - so the pull kept reading the sale the product was born with and put it back every fifteen minutes after the merchant had ended it (measured: ended at 12:26, re-applied at 12:41). That echo is no longer read as a sale. The till's own signals - its documented discount and, where adopted, its campaign price - still reach the shop, and the pull now clears only a sale it set from the till, never one the merchant set in the shop.<\/li>\n<li>Product sync: the sync page keeps showing why a sale was refused by the catalogue until the refusal is actually resolved; a later successful product update no longer wipes the reason.<\/li>\n<li>Product sync: a sale the catalogue refuses is now retried once an hour, not on every run. 4.0.728 marked such a product as unsynced so it would be retried, which meant the product update and the campaign call were both sent every five minutes for as long as the refusal lasted. Measured on the test shop: Surfboard's campaign endpoint currently answers 404 (\"The requested endpoint does not exist\") for every product, so a shop with fifty products on sale would have sent 1,200 calls an hour into a dead route and been rate-limited for everything else. The shop keeps its sale throughout, the sync page names the catalogue's reason, and a sale the merchant changes or ends in the meantime is sent at once.<\/li>\n<li>Product sync: NOTE for merchants - sales set in WooCommerce do not currently reach the till, because the catalogue's campaign endpoint answers 404. The plugin keeps the shop's sale and reports the refusal; the endpoint is Surfboard's to restore.<\/li>\n<\/ul>\n\n<h4>4.0.732<\/h4>\n\n<ul>\n<li>Product sync: a sale set in WooCommerce is no longer cleared by the pull before it has reached the till. A sale is sent to the catalogue as a campaign, on a separate call a minute after the save, and the 15-minute pull could land in that minute: measured on the test shop, a 70 kr sale was read by the pull as present and cleared to nothing, because an unrelated price push a second earlier had marked the product as synced. The pull now keeps a sale that WooCommerce holds but the catalogue has no matching campaign for, and lets the push carry it up.<\/li>\n<\/ul>\n\n<h4>4.0.731<\/h4>\n\n<ul>\n<li>Product sync: when the catalogue refuses a sale (campaign), the reason is now recorded instead of \"(no message)\". The campaign call reported nothing on failure, so a refused sale could not be told apart from a duplicate or a missing route on the sync page or in the log. The catalogue's own words and the HTTP status are now carried through.<\/li>\n<\/ul>\n\n<h4>4.0.730<\/h4>\n\n<ul>\n<li>Product sync: a SKU changed in WooCommerce after the product was created is no longer reverted by the pull. The SKU travels in the catalogue's product properties, which the update call does not carry, so the catalogue keeps the day-one SKU and the pull wrote it back over the merchant's change. The catalogue's copy now only fills an empty SKU.<\/li>\n<li>Product sync: a tax class changed in WooCommerce is no longer silently reverted by the pull. The catalogue takes no VAT change after creation, so the shop's change could never reach the till, and the next pull put the till's rate back without a word. The tax class now follows the catalogue only when the catalogue's rate differs from the rate the shop last saw there - that is, when the till changed it - and a shop\/till mismatch is written to the log once a day, naming both rates, so the side that is wrong can be corrected.<\/li>\n<\/ul>\n\n<h4>4.0.729<\/h4>\n\n<ul>\n<li>Diagnostics: the inventory probe now reads the whole catalogue, page by page, instead of the first page only. On any shop with more than one page of products (about seventy) it answered \"product not found in catalogue\" for every product past the first page - a false answer, measured on the test shop for product after product. An empty page, or a server that repeats page one, ends the read, and a not-found answer now says how many products were actually read.<\/li>\n<\/ul>\n\n<h4>4.0.728<\/h4>\n\n<ul>\n<li>Product sync: the pull no longer downloads a second copy of the shop's own product image. The push sends the featured image as a URL on the shop's own site, the catalogue hands it back, and the pull imported it again - measured on the test shop: one product, one image, two media-library items after a single round trip, and the product's featured image switched to the copy. A URL that resolves to one of the shop's own attachments is now used as it is, for the featured image and the gallery alike, and an image the merchant already chose is never replaced by it.<\/li>\n<li>Product sync: a sale set in WooCommerce is no longer wiped by the pull when the catalogue refused the campaign. The product had already been stamped as pushed by the time the campaign call failed, so the pull saw no pending edit and cleared the sale because the catalogue had none - measured: a 7 kr sale was gone from the shop eight minutes after it was set. A push that falls short now leaves the product unpushed, with the catalogue's reason on the sync page, so the next run retries and the shop keeps its prices meanwhile.<\/li>\n<li>Product sync: saving variations no longer pushes the whole product once per variation. The hook fires for every variation WooCommerce saves, and each firing pushed the parent and all its variants immediately - four full pushes for a four-variation product created over the REST API, the first three sent before the later variations existed. Outside wp-admin the parent now gets one deferred push a minute later; in wp-admin the push still happens at once, but once, after the last variation of the request has been saved.<\/li>\n<\/ul>\n\n<h4>4.0.727<\/h4>\n\n<ul>\n<li>Product sync: a \"&lt;\" that does not start a tag - \"under &lt;10 stk\", \"a &lt; b\" - no longer takes the rest of the sentence with it on the way to the till. PHP's tag stripping treats such a \"&lt;\" as a tag on some PHP versions and not others (measured: the test shop's PHP kept the text, PHP 8.3 returned \"under \"), so what reached the till depended on which PHP the merchant's host runs. The character is now shielded before stripping. Measured on the catalogue: a bare &amp; &lt; &gt; is accepted and stored HTML-encoded, and the pull's comparison sees through that encoding.<\/li>\n<\/ul>\n\n<p><h4>4.0.726<\/h4><\/p>\n\n<ul>\n<li>Product sync: a description with a straight double quote or a backtick now reaches the till. Measured on the test shop across 24 products, one character class each: the catalogue refuses exactly those two characters in the description and nothing else - emoji, HTML, long text, newlines, guillemets and every other punctuation mark went through. A merchant who wrote \"beste kvalitet\" had a product that was refused every five minutes and never reached the iPad. The push now sends the Norwegian typographic forms instead.<\/li>\n<li>Product sync: the pull no longer rewrites a merchant's product text with the catalogue's copy of it. The catalogue only ever holds the tag-free copy the push sent, and writing it back replaced every bold, list and line break in the shop's own description within fifteen minutes of saving. Text that matches the shop's own outbound copy is left alone; only text changed on the till is applied.<\/li>\n<li>Product sync: an edit made in WooCommerce that has not been pushed yet is no longer overwritten by the pull. The push runs a minute after a save and the pull every fifteen, and whenever the push was late, throttled or refused, the pull wrote the catalogue's older name, price and text back over the edit and then stamped the product as pushed - so the edit was lost from both systems. The merchant's name, prices, text and dimensions are now kept while an edit is waiting, and the stamp is withheld so the push still carries it up. Stock keeps following the till throughout. The same rule applies to each variation.<\/li>\n<li>Product sync: the pull no longer republishes - or untrashes - a product the merchant set to draft, private or trash. That revival was meant for products the sync itself had retired, which carry a retirement stamp; without the stamp the status was the merchant's decision and now stands. Catalogue visibility follows the same rule.<\/li>\n<li>Product sync: a short description, weight or dimension changed after the product was created is no longer reverted by the pull. Those fields travel in the catalogue's product properties, which the update call does not carry (by Surfboard's own documentation), so what the catalogue holds is the copy frozen at creation. The pull now uses that copy only to fill an empty field, never to overwrite a later edit.<\/li>\n<li>Product sync: a product created in the catalogue is no longer sent a second time in the same run. The create path never set the marker the dirty query looks for, so every new product was created and then immediately updated with identical data.<\/li>\n<\/ul>\n\n<p><h4>4.0.725<\/h4><\/p>\n\n<ul>\n<li>Product sync: the pull and the push no longer keep each other busy. Measured on the test shop over one hour: 298 variant updates sent to the catalogue, 297 of them for variants where nothing had changed, and 57 rate-limit errors. The pull saved every variable product on every run whether or not anything differed, which stamped it as modified, which made the push re-send every one of its variants. The pull now compares attributes before saving, and the push remembers what the catalogue last accepted for each variant and does not send it again unchanged.<\/li>\n<li>Product sync: a catalogue page that is not empty but repeats page one is now recognised for what it is - a server ignoring the page header, with everything past page one unreadable - and logged as a short read. Measured: Surfboard sends no total in its response headers, so this is the only signal there is; the end of a list is an empty page, which is still treated as the end. Discarding a short read stays behind the saga_catalog_refuse_short_reads filter, off by default, so a server that repeats page one on a one-page catalogue cannot stop a shop's pull.<\/li>\n<\/ul>\n\n<p><h4>4.0.724<\/h4><\/p>\n\n<ul>\n<li>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\".<\/li>\n<\/ul>\n\n<p><h4>4.0.723<\/h4><\/p>\n\n<ul>\n<li>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.<\/li>\n<li>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.<\/li>\n<li><p>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.<\/p>\n\n<p>&hellip;<\/p><\/li>\n<\/ul>","raw_excerpt":"Nordic payment gateway for WooCommerce: Card, Vipps, Klarna, Apple Pay, Google Pay, Swish, MobilePay. Subscriptions, refunds, Blocks, HPOS.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/292686","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=292686"}],"author":[{"embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/sagapay"}],"wp:attachment":[{"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=292686"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=292686"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=292686"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=292686"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=292686"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/arg.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=292686"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}