{"id":393378,"date":"2026-10-28T11:55:00","date_gmt":"2026-10-28T11:55:00","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/paymos-for-woocommerce\/"},"modified":"2026-10-30T16:56:20","modified_gmt":"2026-10-30T16:56:20","slug":"paymos-stablecoin-checkout-for-woocommerce","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/paymos-stablecoin-checkout-for-woocommerce\/","author":23593504,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.5.3","stable_tag":"1.5.3","tested":"7.1.4","requires":"6.5","requires_php":"7.4","requires_plugins":null,"header_name":"Paymos Stablecoin Checkout for WooCommerce","header_author":"Paymos","header_description":"Accept stablecoin payments in WooCommerce through Paymos.","assets_banners_color":"","last_updated":"2026-10-30 16:56:20","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/paymos.io\/docs\/cms-woocommerce","header_author_uri":"https:\/\/paymos.io","rating":0,"author_block_rating":0,"active_installs":0,"downloads":105,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.4.6":{"tag":"1.4.6","author":"paymos","date":"2026-10-28 11:54:26","revision":3770601},"1.5.3":{"tag":"1.5.3","author":"paymos","date":"2026-10-30 16:56:20","revision":3775721}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3770601,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3770601,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.4.6","1.5.3"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[1887,231844,209195,204245,286],"plugin_category":[45],"plugin_contributors":[289198],"plugin_business_model":[],"class_list":["post-393378","plugin","type-plugin","status-publish","hentry","plugin_tags-payments","plugin_tags-stablecoin","plugin_tags-usdc","plugin_tags-usdt","plugin_tags-woocommerce","plugin_category-ecommerce","plugin_contributors-paymos","plugin_committers-paymos"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/paymos-stablecoin-checkout-for-woocommerce\/assets\/icon-128x128.png?rev=3770601","icon_2x":"https:\/\/ps.w.org\/paymos-stablecoin-checkout-for-woocommerce\/assets\/icon-256x256.png?rev=3770601","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Paymos Stablecoin Checkout for WooCommerce supports Sandbox and Live payments, HMAC-signed Merchant API requests, authenticated webhooks, event deduplication, reconciliation, HPOS, and Checkout Blocks.<\/p>\n\n<p>The official package contains no merchant secrets. Connect it from WooCommerce settings using the one-time Paymos device approval flow. Credentials are encrypted with AES-256-GCM using WordPress installation security salts and are never rendered back into the settings page.<\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin connects your WooCommerce store to Paymos. Click <strong>Connect Paymos<\/strong> in the store admin to begin. The plugin starts the device flow at <code>https:\/\/app.paymos.io\/connect\/cms\/device<\/code> and polls <code>https:\/\/app.paymos.io\/connect\/token<\/code>. Your browser opens the Paymos approval page with the plugin identifier, store origin, admin return URL, and <code>user_code<\/code>. The secret <code>device_code<\/code> goes from the plugin to the token endpoint. After approval, the plugin stores Sandbox and Live credentials in WordPress options encrypted with AES-256-GCM, then discards the short-lived OAuth token.<\/p>\n\n<p>The plugin calls <code>https:\/\/api.paymos.io\/v1\/invoices<\/code> to create invoices, <code>GET \/v1\/invoices\/{invoice_id}<\/code> to check status, and <code>POST \/v1\/invoices\/{invoice_id}\/cancel<\/code> to cancel them. Requests carry a key ID, timestamp, and HMAC signature; the API secret signs requests locally. Invoice data includes <code>project_id<\/code>, <code>amount<\/code>, <code>currency<\/code>, and <code>external_order_id<\/code>, plus the native customer ID as <code>client_id<\/code> when available. The WooCommerce order reference includes the order access key.<\/p>\n\n<p>Buyers open the hosted payment page at <code>https:\/\/checkout.paymos.io<\/code> over HTTPS. Paymos sends signed invoice and payment webhooks back to your store so the plugin can update the order.<\/p>\n\n<p>Read the <a href=\"https:\/\/paymos.io\/terms\">Paymos Terms<\/a> and <a href=\"https:\/\/paymos.io\/privacy\">Privacy Policy<\/a> before connecting.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install the official plugin package and activate it.<\/li>\n<li>Open WooCommerce -&gt; Settings -&gt; Payments -&gt; Paymos.<\/li>\n<li>Click Connect Paymos and approve the current project in the Paymos dashboard.<\/li>\n<li>Select Sandbox or Live mode and enable the payment method.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20the%20plugin%20zip%20contain%20my%20api%20secrets%3F\"><h3>Does the plugin ZIP contain my API secrets?<\/h3><\/dt>\n<dd><p>No. Official release packages are identical for every merchant and contain no merchant credentials.<\/p><\/dd>\n<dt id=\"where%20are%20credentials%20stored%3F\"><h3>Where are credentials stored?<\/h3><\/dt>\n<dd><p>In a non-autoloaded WordPress option encrypted with AES-256-GCM. Key material is derived from this WordPress installation's security salts.<\/p><\/dd>\n<dt id=\"which%20project%20is%20connected%3F\"><h3>Which project is connected?<\/h3><\/dt>\n<dd><p>The project currently selected in Paymos when you approve the connection. There is no second project selector.<\/p><\/dd>\n<dt id=\"does%20oauth%20replace%20merchant%20api%20hmac%20authentication%3F\"><h3>Does OAuth replace Merchant API HMAC authentication?<\/h3><\/dt>\n<dd><p>No. OAuth device authorization is only a one-time credential delivery channel. Runtime Merchant API calls remain HMAC signed.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.5.3<\/h4>\n\n<ul>\n<li><p>chore: rebuild canonical CMS package<\/p><\/li>\n<li><p>Compare amounts as exact decimals and require complete invoice snapshots plus API confirmation before changing payment state.<\/p><\/li>\n<li>Serialize order updates and recover when native payment completion returns <code>false<\/code> from save or status updates.<\/li>\n<li>Check ownership when updating event reservations, and purge at most 100 expired records per reservation.<\/li>\n<li>Continue reconciliation in later batches after the first 50 orders, so older pending orders also get checked.<\/li>\n<\/ul>\n\n<p>Fixed:\n* When the checkout could not replace an order's invoice (BUG-166), the order\n  note read \"Paymos payment amount needs manual review.\" although the block\n  also follows a mode or project change, and the buyer read \"Paymos payment\n  error: unable to create invoice.\" (BUG-181). The note is now \"Paymos payment\n  needs manual review.\" followed by the SDK's summary of the old invoice's\n  state, and the buyer reads the SDK's buyer message, \"The store needs to\n  review this order before payment can continue. Please contact the store.\"\n  Both strings are in the <code>.pot<\/code> and translated in every <code>.po<\/code>\/<code>.mo<\/code>.\n* A changed order could leave its old invoice payable beside the new one\n  (BUG-166). When the order total, the mode or the project changed, the\n  checkout cut a new invoice and left the old one open on Paymos, so a buyer\n  could pay both. The old invoice is now cancelled first, in its own\n  environment, through the SDK's <code>InvoiceReplacement<\/code>; the new one is cut only\n  after that cancel succeeds or Paymos reports the old one expired, cancelled\n  or underpaid. When the old invoice is paid, still payable (network picked,\n  funds confirming, part paid) or cannot be read \u2014 a 404 included \u2014 no new\n  invoice is cut and the order is put on hold with a note naming the old\n  invoice.\n* Entries that were present, non-empty and still English \u2014 <code>Connect Paymos<\/code> in\n  German and Spanish, the plugin name in Turkish, the plugin name and\n      Webhook URL in Chinese \u2014 and one string missing from every catalogue\n  (<code>in the invoice currency<\/code>, the fallback in the underpayment notice).\n* The reconciler compared amounts as strings. The snapshot is written with two\n  decimals and the server echoes a fiat amount at the currency's own scale, so a\n  JPY order (<code>2500.00<\/code> against <code>2500<\/code>) never matched and the fallback for a lost\n  webhook skipped it on every run. Amounts are now compared numerically.\n* The reconciler re-applied final statuses every ten minutes. Cancelled and\n  failed orders were fetched and re-noted on each run \u2014 thirty cancellations\n  meant thousands of identical notes and API calls a day \u2014 and, sharing one\n  \"newest 50\" window with them, an order stuck on hold could fall out of reach.\n  Orders whose invoice is already final are no longer fetched, an unchanged\n  status writes nothing, waiting orders get their own window, and an invoice\n  that was simply not paid yet is no longer described as a rolled-back payment.\n* A late non-final webhook could reopen a finished order. Webhooks are\n  delivered at least once and in no particular order, and only paid orders were\n  guarded: an <code>invoice.underpaid_waiting<\/code> or <code>invoice.confirming<\/code> arriving after\n  the invoice had already ended underpaid, expired or cancelled moved the order\n  back into an open state. Nothing leaves a final status on the server, so once\n  one is recorded for an invoice every later event for it is ignored and the\n  final status stays recorded.\n* Paying again for a failed or re-opened order led to a dead invoice. The\n  order kept its <code>external_order_id<\/code>, and the server answers a repeated id with\n  the same invoice \u2014 underpaid, expired or cancelled \u2014 so both checkout and\n  <strong>Pay invoice<\/strong> in My Account sent the buyer to a checkout that could no\n  longer be paid. Such an order now gets a fresh invoice, <strong>Pay invoice<\/strong> is\n  offered only while the invoice is still open, and the invoice deadline is\n  kept on the order.\n* A webhook retry that arrived while the first delivery was still being\n  processed was answered 200 \"duplicate\". Paymos gives a delivery 10 seconds and\n  retries, while a slow reverse-verification call can take longer; the retry was\n  acknowledged as delivered, and if the first attempt then failed the event was\n  lost. An event that is only locked, not yet committed, is now answered 409 so\n  Paymos tries again, and the lock the first delivery holds is left alone.\n* A second invoice could be cut while the first could still be paid. Picking a\n  network moves the invoice deadline on the server (<code>now + PaymentTtl<\/code>) and\n  sends no webhook, but the order kept the deadline from creation; once that\n  passed, paying the order again minted a new <code>external_order_id<\/code> without asking\n  the server. The stored id is now always tried first \u2014 the server answers it\n  with the live invoice \u2014 and a new invoice is cut only if the server says the\n  old one ended unpaid or was never started. An invoice the server holds open\n  (network picked, funds confirming, part paid) is kept, and its status and\n  deadline are refreshed on the order. A stored id from another environment or\n  project gets a new one, since the server refuses it there.<\/p>\n\n<h4>1.5.2<\/h4>\n\n<ul>\n<li><p>fix(plugins): reject invalid Woo refund controls and unblock SDK checks<\/p><\/li>\n<li><p>Compare amounts as exact decimals and require complete invoice snapshots plus API confirmation before changing payment state.<\/p><\/li>\n<li>Serialize order updates and recover when native payment completion returns <code>false<\/code> from save or status updates.<\/li>\n<li>Check ownership when updating event reservations, and purge at most 100 expired records per reservation.<\/li>\n<li>Continue reconciliation in later batches after the first 50 orders, so older pending orders also get checked.<\/li>\n<\/ul>\n\n<p>Fixed:\n* When the checkout could not replace an order's invoice (BUG-166), the order\n  note read \"Paymos payment amount needs manual review.\" although the block\n  also follows a mode or project change, and the buyer read \"Paymos payment\n  error: unable to create invoice.\" (BUG-181). The note is now \"Paymos payment\n  needs manual review.\" followed by the SDK's summary of the old invoice's\n  state, and the buyer reads the SDK's buyer message, \"The store needs to\n  review this order before payment can continue. Please contact the store.\"\n  Both strings are in the <code>.pot<\/code> and translated in every <code>.po<\/code>\/<code>.mo<\/code>.\n* A changed order could leave its old invoice payable beside the new one\n  (BUG-166). When the order total, the mode or the project changed, the\n  checkout cut a new invoice and left the old one open on Paymos, so a buyer\n  could pay both. The old invoice is now cancelled first, in its own\n  environment, through the SDK's <code>InvoiceReplacement<\/code>; the new one is cut only\n  after that cancel succeeds or Paymos reports the old one expired, cancelled\n  or underpaid. When the old invoice is paid, still payable (network picked,\n  funds confirming, part paid) or cannot be read \u2014 a 404 included \u2014 no new\n  invoice is cut and the order is put on hold with a note naming the old\n  invoice.\n* Entries that were present, non-empty and still English \u2014 <code>Connect Paymos<\/code> in\n  German and Spanish, the plugin name in Turkish, the plugin name and\n      Webhook URL in Chinese \u2014 and one string missing from every catalogue\n  (<code>in the invoice currency<\/code>, the fallback in the underpayment notice).\n* The reconciler compared amounts as strings. The snapshot is written with two\n  decimals and the server echoes a fiat amount at the currency's own scale, so a\n  JPY order (<code>2500.00<\/code> against <code>2500<\/code>) never matched and the fallback for a lost\n  webhook skipped it on every run. Amounts are now compared numerically.\n* The reconciler re-applied final statuses every ten minutes. Cancelled and\n  failed orders were fetched and re-noted on each run \u2014 thirty cancellations\n  meant thousands of identical notes and API calls a day \u2014 and, sharing one\n  \"newest 50\" window with them, an order stuck on hold could fall out of reach.\n  Orders whose invoice is already final are no longer fetched, an unchanged\n  status writes nothing, waiting orders get their own window, and an invoice\n  that was simply not paid yet is no longer described as a rolled-back payment.\n* A late non-final webhook could reopen a finished order. Webhooks are\n  delivered at least once and in no particular order, and only paid orders were\n  guarded: an <code>invoice.underpaid_waiting<\/code> or <code>invoice.confirming<\/code> arriving after\n  the invoice had already ended underpaid, expired or cancelled moved the order\n  back into an open state. Nothing leaves a final status on the server, so once\n  one is recorded for an invoice every later event for it is ignored and the\n  final status stays recorded.\n* Paying again for a failed or re-opened order led to a dead invoice. The\n  order kept its <code>external_order_id<\/code>, and the server answers a repeated id with\n  the same invoice \u2014 underpaid, expired or cancelled \u2014 so both checkout and\n  <strong>Pay invoice<\/strong> in My Account sent the buyer to a checkout that could no\n  longer be paid. Such an order now gets a fresh invoice, <strong>Pay invoice<\/strong> is\n  offered only while the invoice is still open, and the invoice deadline is\n  kept on the order.\n* A webhook retry that arrived while the first delivery was still being\n  processed was answered 200 \"duplicate\". Paymos gives a delivery 10 seconds and\n  retries, while a slow reverse-verification call can take longer; the retry was\n  acknowledged as delivered, and if the first attempt then failed the event was\n  lost. An event that is only locked, not yet committed, is now answered 409 so\n  Paymos tries again, and the lock the first delivery holds is left alone.\n* A second invoice could be cut while the first could still be paid. Picking a\n  network moves the invoice deadline on the server (<code>now + PaymentTtl<\/code>) and\n  sends no webhook, but the order kept the deadline from creation; once that\n  passed, paying the order again minted a new <code>external_order_id<\/code> without asking\n  the server. The stored id is now always tried first \u2014 the server answers it\n  with the live invoice \u2014 and a new invoice is cut only if the server says the\n  old one ended unpaid or was never started. An invoice the server holds open\n  (network picked, funds confirming, part paid) is kept, and its status and\n  deadline are refreshed on the order. A stored id from another environment or\n  project gets a new one, since the server refuses it there.<\/p>\n\n<h4>1.5.1<\/h4>\n\n<ul>\n<li><p>fix(plugins): require helper-complete PHP SDK for refunds<\/p><\/li>\n<li><p>Compare amounts as exact decimals and require complete invoice snapshots plus API confirmation before changing payment state.<\/p><\/li>\n<li>Serialize order updates and recover when native payment completion returns <code>false<\/code> from save or status updates.<\/li>\n<li>Check ownership when updating event reservations, and purge at most 100 expired records per reservation.<\/li>\n<li>Continue reconciliation in later batches after the first 50 orders, so older pending orders also get checked.<\/li>\n<\/ul>\n\n<p>Fixed:\n* When the checkout could not replace an order's invoice (BUG-166), the order\n  note read \"Paymos payment amount needs manual review.\" although the block\n  also follows a mode or project change, and the buyer read \"Paymos payment\n  error: unable to create invoice.\" (BUG-181). The note is now \"Paymos payment\n  needs manual review.\" followed by the SDK's summary of the old invoice's\n  state, and the buyer reads the SDK's buyer message, \"The store needs to\n  review this order before payment can continue. Please contact the store.\"\n  Both strings are in the <code>.pot<\/code> and translated in every <code>.po<\/code>\/<code>.mo<\/code>.\n* A changed order could leave its old invoice payable beside the new one\n  (BUG-166). When the order total, the mode or the project changed, the\n  checkout cut a new invoice and left the old one open on Paymos, so a buyer\n  could pay both. The old invoice is now cancelled first, in its own\n  environment, through the SDK's <code>InvoiceReplacement<\/code>; the new one is cut only\n  after that cancel succeeds or Paymos reports the old one expired, cancelled\n  or underpaid. When the old invoice is paid, still payable (network picked,\n  funds confirming, part paid) or cannot be read \u2014 a 404 included \u2014 no new\n  invoice is cut and the order is put on hold with a note naming the old\n  invoice.\n* Entries that were present, non-empty and still English \u2014 <code>Connect Paymos<\/code> in\n  German and Spanish, the plugin name in Turkish, the plugin name and\n      Webhook URL in Chinese \u2014 and one string missing from every catalogue\n  (<code>in the invoice currency<\/code>, the fallback in the underpayment notice).\n* The reconciler compared amounts as strings. The snapshot is written with two\n  decimals and the server echoes a fiat amount at the currency's own scale, so a\n  JPY order (<code>2500.00<\/code> against <code>2500<\/code>) never matched and the fallback for a lost\n  webhook skipped it on every run. Amounts are now compared numerically.\n* The reconciler re-applied final statuses every ten minutes. Cancelled and\n  failed orders were fetched and re-noted on each run \u2014 thirty cancellations\n  meant thousands of identical notes and API calls a day \u2014 and, sharing one\n  \"newest 50\" window with them, an order stuck on hold could fall out of reach.\n  Orders whose invoice is already final are no longer fetched, an unchanged\n  status writes nothing, waiting orders get their own window, and an invoice\n  that was simply not paid yet is no longer described as a rolled-back payment.\n* A late non-final webhook could reopen a finished order. Webhooks are\n  delivered at least once and in no particular order, and only paid orders were\n  guarded: an <code>invoice.underpaid_waiting<\/code> or <code>invoice.confirming<\/code> arriving after\n  the invoice had already ended underpaid, expired or cancelled moved the order\n  back into an open state. Nothing leaves a final status on the server, so once\n  one is recorded for an invoice every later event for it is ignored and the\n  final status stays recorded.\n* Paying again for a failed or re-opened order led to a dead invoice. The\n  order kept its <code>external_order_id<\/code>, and the server answers a repeated id with\n  the same invoice \u2014 underpaid, expired or cancelled \u2014 so both checkout and\n  <strong>Pay invoice<\/strong> in My Account sent the buyer to a checkout that could no\n  longer be paid. Such an order now gets a fresh invoice, <strong>Pay invoice<\/strong> is\n  offered only while the invoice is still open, and the invoice deadline is\n  kept on the order.\n* A webhook retry that arrived while the first delivery was still being\n  processed was answered 200 \"duplicate\". Paymos gives a delivery 10 seconds and\n  retries, while a slow reverse-verification call can take longer; the retry was\n  acknowledged as delivered, and if the first attempt then failed the event was\n  lost. An event that is only locked, not yet committed, is now answered 409 so\n  Paymos tries again, and the lock the first delivery holds is left alone.\n* A second invoice could be cut while the first could still be paid. Picking a\n  network moves the invoice deadline on the server (<code>now + PaymentTtl<\/code>) and\n  sends no webhook, but the order kept the deadline from creation; once that\n  passed, paying the order again minted a new <code>external_order_id<\/code> without asking\n  the server. The stored id is now always tried first \u2014 the server answers it\n  with the live invoice \u2014 and a new invoice is cut only if the server says the\n  old one ended unpaid or was never started. An invoice the server holds open\n  (network picked, funds confirming, part paid) is kept, and its status and\n  deadline are refreshed on the order. A stored id from another environment or\n  project gets a new one, since the server refuses it there.<\/p>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>fix(plugins): complete CMS refund release dependencies<\/li>\n<li>fix(ton): return friendly addresses with existing webhook schema<\/li>\n<li>fix(woocommerce): refuse offline refunds and refunded status on Paymos orders<\/li>\n<li>feat(woocommerce): gate native refunds on the connection's refund capability<\/li>\n<li>fix(woocommerce): show refund errors on the native button and in the panel<\/li>\n<li>feat(woocommerce): copy for the native refund panel and its notices<\/li>\n<li>feat(woocommerce): native refund adapter for full refunds and overpayment notes<\/li>\n<li>chore: bundle Paymos PHP SDK v1.8.0<\/li>\n<li><p>chore: rebuild canonical CMS package<\/p><\/li>\n<li><p>Compare amounts as exact decimals and require complete invoice snapshots plus API confirmation before changing payment state.<\/p><\/li>\n<li>Serialize order updates and recover when native payment completion returns <code>false<\/code> from save or status updates.<\/li>\n<li>Check ownership when updating event reservations, and purge at most 100 expired records per reservation.<\/li>\n<li>Continue reconciliation in later batches after the first 50 orders, so older pending orders also get checked.<\/li>\n<\/ul>\n\n<p>Fixed:\n* When the checkout could not replace an order's invoice (BUG-166), the order\n  note read \"Paymos payment amount needs manual review.\" although the block\n  also follows a mode or project change, and the buyer read \"Paymos payment\n  error: unable to create invoice.\" (BUG-181). The note is now \"Paymos payment\n  needs manual review.\" followed by the SDK's summary of the old invoice's\n  state, and the buyer reads the SDK's buyer message, \"The store needs to\n  review this order before payment can continue. Please contact the store.\"\n  Both strings are in the <code>.pot<\/code> and translated in every <code>.po<\/code>\/<code>.mo<\/code>.\n* A changed order could leave its old invoice payable beside the new one\n  (BUG-166). When the order total, the mode or the project changed, the\n  checkout cut a new invoice and left the old one open on Paymos, so a buyer\n  could pay both. The old invoice is now cancelled first, in its own\n  environment, through the SDK's <code>InvoiceReplacement<\/code>; the new one is cut only\n  after that cancel succeeds or Paymos reports the old one expired, cancelled\n  or underpaid. When the old invoice is paid, still payable (network picked,\n  funds confirming, part paid) or cannot be read \u2014 a 404 included \u2014 no new\n  invoice is cut and the order is put on hold with a note naming the old\n  invoice.\n* Entries that were present, non-empty and still English \u2014 <code>Connect Paymos<\/code> in\n  German and Spanish, the plugin name in Turkish, the plugin name and\n      Webhook URL in Chinese \u2014 and one string missing from every catalogue\n  (<code>in the invoice currency<\/code>, the fallback in the underpayment notice).\n* The reconciler compared amounts as strings. The snapshot is written with two\n  decimals and the server echoes a fiat amount at the currency's own scale, so a\n  JPY order (<code>2500.00<\/code> against <code>2500<\/code>) never matched and the fallback for a lost\n  webhook skipped it on every run. Amounts are now compared numerically.\n* The reconciler re-applied final statuses every ten minutes. Cancelled and\n  failed orders were fetched and re-noted on each run \u2014 thirty cancellations\n  meant thousands of identical notes and API calls a day \u2014 and, sharing one\n  \"newest 50\" window with them, an order stuck on hold could fall out of reach.\n  Orders whose invoice is already final are no longer fetched, an unchanged\n  status writes nothing, waiting orders get their own window, and an invoice\n  that was simply not paid yet is no longer described as a rolled-back payment.\n* A late non-final webhook could reopen a finished order. Webhooks are\n  delivered at least once and in no particular order, and only paid orders were\n  guarded: an <code>invoice.underpaid_waiting<\/code> or <code>invoice.confirming<\/code> arriving after\n  the invoice had already ended underpaid, expired or cancelled moved the order\n  back into an open state. Nothing leaves a final status on the server, so once\n  one is recorded for an invoice every later event for it is ignored and the\n  final status stays recorded.\n* Paying again for a failed or re-opened order led to a dead invoice. The\n  order kept its <code>external_order_id<\/code>, and the server answers a repeated id with\n  the same invoice \u2014 underpaid, expired or cancelled \u2014 so both checkout and\n  <strong>Pay invoice<\/strong> in My Account sent the buyer to a checkout that could no\n  longer be paid. Such an order now gets a fresh invoice, <strong>Pay invoice<\/strong> is\n  offered only while the invoice is still open, and the invoice deadline is\n  kept on the order.\n* A webhook retry that arrived while the first delivery was still being\n  processed was answered 200 \"duplicate\". Paymos gives a delivery 10 seconds and\n  retries, while a slow reverse-verification call can take longer; the retry was\n  acknowledged as delivered, and if the first attempt then failed the event was\n  lost. An event that is only locked, not yet committed, is now answered 409 so\n  Paymos tries again, and the lock the first delivery holds is left alone.\n* A second invoice could be cut while the first could still be paid. Picking a\n  network moves the invoice deadline on the server (<code>now + PaymentTtl<\/code>) and\n  sends no webhook, but the order kept the deadline from creation; once that\n  passed, paying the order again minted a new <code>external_order_id<\/code> without asking\n  the server. The stored id is now always tried first \u2014 the server answers it\n  with the live invoice \u2014 and a new invoice is cut only if the server says the\n  old one ended unpaid or was never started. An invoice the server holds open\n  (network picked, funds confirming, part paid) is kept, and its status and\n  deadline are refreshed on the order. A stored id from another environment or\n  project gets a new one, since the server refuses it there.<\/p>\n\n<h4>1.4.7<\/h4>\n\n<ul>\n<li>chore: commit pending Forge records, WordPress packaging and social drafts<\/li>\n<li><p>chore: rebuild canonical CMS package<\/p><\/li>\n<li><p>Compare amounts as exact decimals and require complete invoice snapshots plus API confirmation before changing payment state.<\/p><\/li>\n<li>Serialize order updates and recover when native payment completion returns <code>false<\/code> from save or status updates.<\/li>\n<li>Check ownership when updating event reservations, and purge at most 100 expired records per reservation.<\/li>\n<li>Continue reconciliation in later batches after the first 50 orders, so older pending orders also get checked.<\/li>\n<\/ul>\n\n<p>Fixed:\n* When the checkout could not replace an order's invoice (BUG-166), the order\n  note read \"Paymos payment amount needs manual review.\" although the block\n  also follows a mode or project change, and the buyer read \"Paymos payment\n  error: unable to create invoice.\" (BUG-181). The note is now \"Paymos payment\n  needs manual review.\" followed by the SDK's summary of the old invoice's\n  state, and the buyer reads the SDK's buyer message, \"The store needs to\n  review this order before payment can continue. Please contact the store.\"\n  Both strings are in the <code>.pot<\/code> and translated in every <code>.po<\/code>\/<code>.mo<\/code>.\n* A changed order could leave its old invoice payable beside the new one\n  (BUG-166). When the order total, the mode or the project changed, the\n  checkout cut a new invoice and left the old one open on Paymos, so a buyer\n  could pay both. The old invoice is now cancelled first, in its own\n  environment, through the SDK's <code>InvoiceReplacement<\/code>; the new one is cut only\n  after that cancel succeeds or Paymos reports the old one expired, cancelled\n  or underpaid. When the old invoice is paid, still payable (network picked,\n  funds confirming, part paid) or cannot be read \u2014 a 404 included \u2014 no new\n  invoice is cut and the order is put on hold with a note naming the old\n  invoice.\n* Entries that were present, non-empty and still English \u2014 <code>Connect Paymos<\/code> in\n  German and Spanish, the plugin name in Turkish, the plugin name and\n      Webhook URL in Chinese \u2014 and one string missing from every catalogue\n  (<code>in the invoice currency<\/code>, the fallback in the underpayment notice).\n* The reconciler compared amounts as strings. The snapshot is written with two\n  decimals and the server echoes a fiat amount at the currency's own scale, so a\n  JPY order (<code>2500.00<\/code> against <code>2500<\/code>) never matched and the fallback for a lost\n  webhook skipped it on every run. Amounts are now compared numerically.\n* The reconciler re-applied final statuses every ten minutes. Cancelled and\n  failed orders were fetched and re-noted on each run \u2014 thirty cancellations\n  meant thousands of identical notes and API calls a day \u2014 and, sharing one\n  \"newest 50\" window with them, an order stuck on hold could fall out of reach.\n  Orders whose invoice is already final are no longer fetched, an unchanged\n  status writes nothing, waiting orders get their own window, and an invoice\n  that was simply not paid yet is no longer described as a rolled-back payment.\n* A late non-final webhook could reopen a finished order. Webhooks are\n  delivered at least once and in no particular order, and only paid orders were\n  guarded: an <code>invoice.underpaid_waiting<\/code> or <code>invoice.confirming<\/code> arriving after\n  the invoice had already ended underpaid, expired or cancelled moved the order\n  back into an open state. Nothing leaves a final status on the server, so once\n  one is recorded for an invoice every later event for it is ignored and the\n  final status stays recorded.\n* Paying again for a failed or re-opened order led to a dead invoice. The\n  order kept its <code>external_order_id<\/code>, and the server answers a repeated id with\n  the same invoice \u2014 underpaid, expired or cancelled \u2014 so both checkout and\n  <strong>Pay invoice<\/strong> in My Account sent the buyer to a checkout that could no\n  longer be paid. Such an order now gets a fresh invoice, <strong>Pay invoice<\/strong> is\n  offered only while the invoice is still open, and the invoice deadline is\n  kept on the order.\n* A webhook retry that arrived while the first delivery was still being\n  processed was answered 200 \"duplicate\". Paymos gives a delivery 10 seconds and\n  retries, while a slow reverse-verification call can take longer; the retry was\n  acknowledged as delivered, and if the first attempt then failed the event was\n  lost. An event that is only locked, not yet committed, is now answered 409 so\n  Paymos tries again, and the lock the first delivery holds is left alone.\n* A second invoice could be cut while the first could still be paid. Picking a\n  network moves the invoice deadline on the server (<code>now + PaymentTtl<\/code>) and\n  sends no webhook, but the order kept the deadline from creation; once that\n  passed, paying the order again minted a new <code>external_order_id<\/code> without asking\n  the server. The stored id is now always tried first \u2014 the server answers it\n  with the live invoice \u2014 and a new invoice is cut only if the server says the\n  old one ended unpaid or was never started. An invoice the server holds open\n  (network picked, funds confirming, part paid) is kept, and its status and\n  deadline are refreshed on the order. A stored id from another environment or\n  project gets a new one, since the server refuses it there.<\/p>","raw_excerpt":"Accept stablecoin payments with Paymos hosted checkout and signed webhooks.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/393378","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=393378"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/paymos"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=393378"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=393378"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=393378"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=393378"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=393378"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=393378"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}