=== Post Coverage for WooCommerce === Contributors: postcoverage Tags: woocommerce, package protection, shipping protection, checkout, order protection Requires at least: 6.4 Tested up to: 7.1 Requires PHP: 8.0 Stable tag: 1.4.1 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html Opt-in package protection above Place Order. Post Coverage quotes the fee, records each paid USD checkout order and keeps delivery events in step. == Description == Post Coverage protects a shipment after the customer has paid for it. This plugin puts the offer in your WooCommerce checkout, charges the fee Post Coverage quotes, records the order with Post Coverage, and tells Post Coverage when the order ships, is cancelled or is refunded. If a parcel is lost, stolen or damaged, the customer opens a claim with Post Coverage using a private claim link. Post Coverage is built for online stores that sell everyday merchandise: clothing, beauty, electronics, home goods and gifts. This plugin runs on your own WordPress server, so you control the data and what leaves it. Post Coverage also supports Shopify (the app is in development), Medusa, its Node.js and PHP libraries, and a REST API for any other platform. **Sandbox release.** The Post Coverage API is in sandbox. Its quotes are illustrative and it issues no live coverage yet, so do not charge real customers a protection fee until Post Coverage confirms an approved merchant agreement for your store. See the FAQ for what live mode can and cannot do today. = The protection module = * **Checkout only, directly above the Place Order button**, in both the classic (shortcode) checkout and the Checkout block. The switch is not on the cart page or in the mini cart. Once a customer has switched protection on at checkout, the fee is part of their cart, so its line can also appear in the totals of the cart page and the mini cart; it is switched off at checkout. * **Off until the customer turns it on.** Protection starts switched off on every order. There is no setting, filter or default that pre-selects it, and it is never carried into a subscription renewal. With protection off the module reads "No protection added." When the order earns store credit, the line under the title says so ("+$16.00 ยท earns $45.70 in store credit"), and with protection on a note beneath the module reads "Credited to your account as store credit." * **One module, one fee.** The classic and block checkouts share the same state, so loading both can never add the fee twice. * **Only when something ships.** A cart of virtual products only, such as an e-gift card, has no parcel to protect, so it is offered nothing: no module, no quote request and no fee. A customer who switched protection on and then removed the last item that ships is switched off and charged nothing. A cart with at least one item that ships is offered protection on what ships: digital (virtual) items are not covered, so they add nothing to the fee or the protected amount. Whether a cart ships is WooCommerce's own answer, so on a store with shipping switched off, or with no shipping method in any zone, the module never appears; the settings screen says so. = The fee is quoted by Post Coverage, never computed in the store = The plugin sends Post Coverage the merchandise subtotal after discounts, excluding shipping, tax and the protection fee itself, and charges exactly the fee that comes back. It never applies a rate of its own and never trusts a price from the browser. A change to the cart gets a fresh quote. If no quote can be obtained, the module is not shown rather than showing a guessed price, and the checkout never charges a fee the page did not show. The API quotes US dollars only, for orders of $1.00 or more. Below that, or in another currency, the module is not shown. Post Coverage can switch the protection offer off for a store, for example while it reviews what the store sells. Checkout then shows no module and no message, the way it does for a cart outside the priced range, and the settings screen says why. A price already quoted can still be shown for up to 15 minutes, and orders already placed, including one paid at a price quoted before the switch, are recorded as usual. While the offer is off the plugin asks again every 15 minutes, so it spends almost none of your request allowance, and the module returns by itself once Post Coverage switches the offer back on. Test connection checks straight away. = Store credit = When your Post Coverage reward program is active, each quote includes a preview of the store credit a protected order would earn, and the module shows it next to the fee. It is store credit, scheduled by Post Coverage for the customer's account the number of days after the order that your reward program sets. That timing does not wait for a confirmed delivery, and a refund or cancellation reduces or cancels the credit. It is never cash and never a refund to a card. Guest customers earn it too. This plugin shows the preview only: it does not display or redeem store credit in your store. = Recording orders and keeping them in step = * **Each paid checkout order is recorded** once payment completes: protected, declined and ineligible alike, so Post Coverage's reports describe the whole store and not just the protected orders. An order with nothing to ship (virtual products only) was never offered protection, so it is recorded as ineligible rather than as a decline. Some paid orders are not sent. Orders keyed in by hand in the admin never passed through the checkout and have no recorded choice, and orders in a currency other than US dollars cannot be recorded; both are skipped, with a line in the log. An order Post Coverage would reject, such as one whose protected amount is below $1.00, is listed as failed on the sync screen with the reason. * **Order creation is never repeated automatically.** It returns a once-only claim access code, so when the outcome of a request is unclear the plugin reads the order back from Post Coverage instead of sending it again. * **Lifecycle events.** A completed order is reported as in transit, and cancelled and refunded orders are reported too. A refund reports only refunded merchandise: a refund of shipping or other fee rows reports nothing, a refund that includes the protection fee line also reports that fee (refunding all of it ends protection on the order), and an amount typed into "Refund amount" without choosing rows is taken to refund the order's unrefunded tax, shipping and fees first, so only what is left over is reported as merchandise. Delivered and delivery-exception events come from the Post Coverage order actions, or from a shipping integration calling `do_action( 'post_coverage_woo_record_event', $order, 'delivered' )`. * **A durable job table.** Every background job is one row in a dedicated database table, delivered through Action Scheduler one request at a time with retries and backoff. **WooCommerce > Post Coverage sync** lists anything not yet delivered with the reason, and retries one job, a selection, or every blocked job at once. * **Issue a new claim link.** The order action "Post Coverage: issue a new claim link" replaces a customer's claim access code and revokes the old one. * **The claim link email.** "Package protection: claim link" (email ID `post_coverage_claim_link`) sends the customer's private claim link to the order's billing email address when the order is recorded with Post Coverage, and again when a new claim link is issued. It is on by default. Edit it or switch it off under WooCommerce > Settings > Emails. It has no Cc or Bcc, and a copy or extra recipient added by a filter is dropped. If you already deliver the claim link through your own email, switch this one off so the customer does not get it twice. * **The claim link in My Account.** A logged-in customer sees a "Package protection" box with the claim link on their own order in My Account. It is not shown on the order-received page or in order tracking. * **The claim update email.** "Package protection: claim update" (email ID `post_coverage_claim_update`) tells a customer that there is news on their delivery claim: a reply from the Post Coverage support team, or a change of status. It says only that: "There's news on the delivery claim for order #1234. Open your claim to see it:", their private claim link, and "For your privacy, we never put claim details in email." It never carries the reply, the status or anything else about the claim. It goes to the order's billing email address only, at most once every six hours for each claim (news that arrives sooner waits, and the next email covers it), and never twice for the same news. If the order has no claim link that can be sent at that moment, it links the customer's order page instead. It is on by default; edit it or switch it off under WooCommerce > Settings > Emails. In sandbox mode it is sent for sandbox claims; in live mode it is not, and it is never sent for an order recorded in live mode. The plugin checks your store's claims every 15 minutes for it, one to three requests from your allowance (see below). News from before you installed this version, or more than a week old, is not emailed. * **Order actions name the address.** When the email is on, "Post Coverage: issue a new claim link and email it to
" revokes the old link and emails the new one to the billing address named in the action. "Post Coverage: issue a new claim link without emailing it" revokes the old link and sends nothing. "Post Coverage: email the claim link to " sends the current link again. If the billing address is wrong, correct it in the same save or issue the new link without emailing it. = What the plugin does not do = * **It sends the claim link to nobody but the customer.** The email goes only to the order's billing email address. To deliver the link yourself, read it with `PC_Orders::claim_link( $order )` (the `post_coverage_woo_claim_access_reissued` action fires with the order whenever a new link is issued), and switch the plugin's email off. * **It never shows or logs the access code.** The code is a credential. It is stored encrypted on the order, never printed on an admin screen, and redacted from every log line. * **It never sends customer identity.** See External services below. = Compatibility = * WooCommerce high-performance order storage (HPOS): supported and declared. Orders are only read and written through the WooCommerce order API. * Cart and Checkout blocks: supported and declared. * WooCommerce Subscriptions: the protection fee and the protection choice are kept out of renewal orders. This has been checked against the extension's source but not tested with the extension installed. = Bundled fonts = The module uses Inter Tight and DM Sans, bundled as WOFF2 files in `assets/fonts/` so nothing is loaded from a font service. Both are licensed under the SIL Open Font License 1.1; the licence texts are `assets/fonts/OFL-Inter-Tight.txt` and `assets/fonts/OFL-DM-Sans.txt`. = Source code = `build/checkout-block.js` is compiled from `src/js/checkout-block.js`, which ships in the plugin. To rebuild it, run `npm install` and `npm run build` in the plugin folder (it uses `@wordpress/scripts`). == External services == This plugin connects to the Post Coverage API, a service run by Post Coverage, at the API URL you enter in the settings. The API is what quotes the protection fee, records orders and their delivery events, and holds the claim that a customer opens. The plugin cannot work without it. Every call is made from your web server. Customers' browsers never contact the API, and the API key is never sent to a browser. Nothing is sent until you have saved an API URL and key and turned on "Offer Post Coverage protection to customers", except the connection test, which you start yourself. What is sent, and when: * **Quote** (`POST /integrations/v1/quotes`): when a customer views or updates the checkout with a cart that has something to ship and has not been quoted recently, and once each time you press Test connection. Sends the merchandise subtotal in cents and the currency. Quotes are cached and shared across visitors, so repeat views cost nothing. * **Order** (`POST /integrations/v1/orders`): once, after payment completes on an order placed through your checkout. Sends an order reference (the WooCommerce order ID, which is the internal database ID and not a customised order number, prefixed with a short hash of your site address, meaningless outside your store), the protected amount in cents, the currency, whether the purchase is one-off or recurring, whether the customer chose protection, whether the order is eligible for it, the pricing version of the quote, and a customer key. The customer key is a keyed hash, under a secret that never leaves your site, of the customer's account number or, for a guest, of their lower-cased billing email address. The email address itself is never sent, and cannot be recovered from the key. * **Order read-back** (`GET /integrations/v1/orders/detail`): to settle an order creation whose outcome was unclear, and just before each lifecycle event or claim link reset, to read the order's current version. Sends the Post Coverage order reference. * **Lifecycle event** (`POST /integrations/v1/orders/events`): when an order is completed, cancelled or refunded, when you use a Post Coverage order action, or when a shipping integration reports a delivery. Sends the Post Coverage order reference, the event type, a random request id, the expected order version and, for a refund, the refunded merchandise amount in cents; for a refund of part of the protection fee, that part in cents. * **Claim link reset** (`POST /integrations/v1/orders/claim-access`): only when you choose one of the "Post Coverage: issue a new claim link" order actions on an order. Sends the Post Coverage order reference, a random request id and the expected order version. * **Claims** (`GET /integrations/v1/claims`): every 15 minutes while the claim update email is switched on, one to three pages of your store's claims, one request each. Sends nothing but the page size and where the last page ended. Each claim comes back with its reference, order reference, status and the time of its last update for the customer, never a message. * **Health check** (`GET /health`): when you press Test connection. Sends nothing but the request; no key and no order data. Every request except the health check carries your API key in a header. Every request carries a user agent naming this plugin and its version (`PostCoverageWoo/` and the version number). Post Coverage records that version, and when your store last made each kind of request, so its team can see that the plugin is installed and whether an update is available without any access to your store. In sandbox mode the event and claim link reset requests are also marked synthetic. The order request also sends the order's products (2026-10-06): for each line, its name, SKU, line number, quantity, unit price and whether it ships, so Post Coverage can show them to your customer's claim and to its team. Digital (virtual) products are sent as not shipping: they are not covered, and they are left out of the protected amount. Never sent: customer names, email addresses, phone numbers, postal addresses, payment details or IP addresses. Post Coverage terms of service: https://www.postcoverage.com/terms/ Post Coverage privacy policy: https://www.postcoverage.com/privacy/ == Installation == 1. Install and activate WooCommerce 8.0 or newer. 2. In **Plugins > Add New > Upload Plugin**, upload `post-coverage-woo.zip` and activate it. 3. Go to **WooCommerce > Post Coverage**. 4. Enter the API URL and the server key Post Coverage issued to you. The URL is a bare HTTPS origin with no path. 5. Press **Test connection**. It checks that the API answers, then asks for one quote to prove the key works, and says whether your reward program is active. 6. Tick **Offer Post Coverage protection to customers** when you are ready for the module to appear at checkout. If you installed the earlier "Post Coverage Sandbox" developer scaffold, deactivate and delete it first. It is not this plugin and it is no longer supported. == Frequently Asked Questions == = Is there a version for Shopify? = Post Coverage also supports Shopify. Its Shopify app is in development; this plugin is for WooCommerce stores only. = Can I make protection selected by default? = No, deliberately. Protection is the customer's choice on every order and starts switched off. The setting under WooCommerce > Post Coverage turns the offer on for your store; it does not, and cannot, turn protection on for a customer. = What is the difference between sandbox and live mode? = Sandbox mode is for test orders only. It marks the lifecycle events and claim link resets of every order the store records as synthetic, which the Post Coverage API currently requires before it accepts them, because the plugin cannot tell a test order from a real one. A store taking real orders must not run in sandbox mode. Live mode never marks anything synthetic, and needs a second confirmation before it can be saved. Live mode is not a way to take real protection fees yet. The API accepts quotes and orders without the synthetic mark, but it issues no live coverage: its quotes are illustrative and no policy stands behind them. So in live mode the plugin offers protection only for a quote Post Coverage marks as live, and today none is: the module is not shown, no fee is charged, and the settings screen says why. Do not charge real customers a protection fee, in either mode, until Post Coverage confirms an approved merchant agreement for your store. In live mode the API also refuses two kinds of request, because it still requires the synthetic mark on them and this plugin will not put that mark on a real customer's order: * **Lifecycle events.** A refused event is kept on the Post Coverage sync screen as blocked, never deleted, and "Retry all blocked" sends it again once Post Coverage accepts live events. * **Claim link resets.** A refused reset is not queued and is not sent again automatically. Nothing changes, the claim link issued before is left as it was, and an order note records the refusal. Ask Post Coverage support to reissue the link. = How many requests can the site make? = The API allows 40 requests in each fixed five-minute window, counted per originating IP address, so everything on your server shares one allowance. The plugin counts every request it sends in one place in your database, and the Post Coverage Claim Lookup plugin, when it is installed on the same site, counts its lookups in the same place, so together they stay inside the 40. The settings screen shows that combined count. The plugin holds part of the allowance back so a customer at checkout is not blocked by a background job, and backs off when the allowance runs out. A checkout quote is one request and is never retried. Each visitor may also cause only a few fresh quotes per five minutes, so one person changing their cart repeatedly cannot use up the store's allowance. Visitors are told apart by a keyed hash of their IP address; the address itself is not stored. = Can a customer's browser call Post Coverage directly? = No, and it should not. The API accepts browser requests only from Post Coverage's own sites, and the server key must never reach a browser. Every call this plugin makes is made from PHP. = What happens if a call fails halfway? = Order creation and claim link resets are never retried automatically, because each hands back a claim access code exactly once. When the outcome cannot be established, the order is marked for reconciliation and the plugin reads the order back from Post Coverage. Everything else is retried with backoff, and anything that still fails is listed on the Post Coverage sync screen. = Does the plugin log anything sensitive? = No. The API key, access codes and the code in a claim link are replaced with a placeholder before anything is written, including inside nested data. Logs are under **WooCommerce > Status > Logs**, source `post-coverage`. = Does the plugin email my customers? = Two emails, to the billing email address of a protected order, and nobody else: * its private claim link, once when the order is recorded and again if you issue a new link. You can send it again from the order with "Post Coverage: email the claim link to "; * when the customer has opened a claim and there is news on it, a short note that there is an update, with the same claim link: at most one every six hours for each claim, and never the update itself. Nothing is sent for orders without protection. You can edit either email or switch it off like any WooCommerce email. The link is a credential: if a mail logging plugin stores copies of sent email, treat those logs as sensitive, and keep click tracking off for these emails. A claim update email that could not be sent is listed as failed on the Post Coverage sync screen, and Retry sends it again. = Is the plugin translatable? = Yes. The text domain is `post-coverage-woo` and a template is in `languages/post-coverage-woo.pot`. Translations placed in the plugin's `languages/` folder are loaded, including those for the Checkout block's script. = What does uninstalling remove? = The plugin's settings and queue state, the claim link and claim update emails' settings, the claim update check's state, its job table, and the transients it keeps: the connection-test result, admin notices and internal flags (under its own `pc_woo_` prefix), the quote cache, the pause after a failed quote and the note that Post Coverage has switched the offer off (`pc_quote_`), the per-visitor quote counters (`pc_qv_`), and the two emails' previews (`woocommerce_post_coverage_claim_link_` and `woocommerce_post_coverage_claim_update_`). On a site with an external object cache those transients live in the cache instead, and expire there on their own within a day. Its scheduled jobs are stopped: the WP-Cron events are removed, and pending Action Scheduler actions are cancelled so they never run. Three kinds of record are held by WooCommerce and left to its own clean-up: what a shopper's checkout session holds for this plugin (their protection choice, the fee and pricing version quoted for it, and whether that fee was added), which goes when WooCommerce expires the session; Action Scheduler's record of this plugin's actions, both those already run and those cancelled at uninstall, which Action Scheduler purges after its retention period (a month by default); and this plugin's entries in **WooCommerce > Status > Logs** (source `post-coverage`), which WooCommerce deletes under its own log retention. The request counter this plugin shares with the Post Coverage Claim Lookup plugin (the `pc_api_budget_v1` option) is removed as well, unless Claim Lookup is still installed. Claim Lookup still counts in it, and removes it when it is uninstalled itself. Order records are left alone: the Post Coverage order reference, the encrypted access code and the record of which code and which claim update were emailed stay on each order, because deleting a plugin is not a decision to destroy the record of what a customer bought. The site hash and merchant id that order references are built from are kept too, so a reinstall recognises the same orders. Jobs still waiting on the Post Coverage sync screen are lost, so check it before deleting the plugin. == Changelog == = 1.4.1 = * The protection module's heading font is now Inter Tight, bundled as a WOFF2 file like before (it replaces Bricolage Grotesque). Body text stays DM Sans. = 1.4.0 = * Orders now carry their products (name, SKU, quantity, unit price, and whether each ships), so a customer can say which items a claim is about. If Post Coverage's API is older and refuses them, the order is sent again without them. * Digital items are not covered: a virtual product is left out of the quoted and protected amount, so a mixed cart pays a fee on what ships only, and refunding a virtual product on an order that ships something reports nothing to Post Coverage. * Refunding the protection fee is reported to Post Coverage. A refund that includes the order's protection fee line (recognised by the plugin's own mark on that line, never by its name or amount) is sent after any merchandise as a fee refund: refunding the whole fee ends protection on the order, shipped or not, and part of it is noted for Post Coverage's staff. A refund typed as an amount only is never read as a fee refund. The sync screen lists it as "protection fee refunded". = 1.3.0 = * No maximum order value. Carts and orders above $10,000.00 are now quoted, protected and recorded like any other; only an amount below $1.00 gets no offer. * The sync screen's reason for a refused amount now reads "below the 100 cents Post Coverage accepts". = 1.2.1 = * Fixed: on a store where nobody opens the WordPress admin, such as one set up with WP-CLI or updated automatically while its owner was away, 1.2.0 never scheduled its 15-minute claim check or its hourly queue housekeeping, so no claim update email was ever sent. Both were scheduled only when a store manager loaded an admin page. They are now also scheduled by the plugin's own background work (its job queue, and Action Scheduler's or WP-Cron's regular run), straight away after activation or an update, and still never while a shopper's page loads. * The checks stay cheap: at most once an hour each, except the first after activation or an update. = 1.2.0 = * New "Package protection: claim update" email. When the Post Coverage support team replies to a customer's claim or moves it on, the customer gets a short email from your store: "There's an update on your claim", the order number, their private claim link, and "For your privacy, we never put claim details in email." It never says what the update is. At most one every six hours for each claim, never twice for the same news, to the billing email address only, and on by default. Edit it or switch it off under WooCommerce > Settings > Emails. * The plugin checks your store's claims for news every 15 minutes, through the same queue and request allowance as its other background work: one to three requests each time, never retried in between. With the email switched off it checks nothing. News from more than a day before the first check, or before the email is switched back on after more than a day off, is not emailed. * A claim update email that could not be sent is listed as failed on the Post Coverage sync screen, where Retry sends it again, and every one that was sent is recorded there. * Sandbox claims are emailed about only in sandbox mode, and never for an order recorded in live mode. Retrying a failed claim update email checks this again. * `PC_Client::claims( $cursor, $page_size )` reads one page of the store's claims. = 1.1.0 = * When Post Coverage switches the protection offer off for your store, the checkout hides the module quietly instead of saying that protection could not be priced. The plugin asks again every 15 minutes rather than every 30 seconds, and the module returns by itself when the offer is switched back on. Orders already placed are recorded as before. * The settings screen says when Post Coverage has switched the offer off, and Test connection reports it as "Key accepted" rather than as a failure. * An order whose pricing changed after checkout, at a store whose offer Post Coverage has since switched off, is still checked against the current price and recorded, with the offer still off. If the fee changed it waits for your decision on the sync screen, as before. * `PC_Api_Exception` carries the API's refusal code in `$api_code` ('' when there is none), and the current price sent with an offer-off refusal in `$current_price` (array() when there is none). = 1.0.0 = * First release. * Protection module at checkout only, above the Place Order button, in the classic and block checkouts. Off by default for every order, with no setting to change that. * Fee quoted by Post Coverage for the merchandise subtotal after discounts, net of tax, re-quoted on every cart change, with a per-visitor limit on fresh quotes. A checkout quote is sent once and never retried. * An order is placed only at the protection fee its checkout page showed. If the price changed or is gone by the time the customer presses Place Order, the checkout stops and shows the current price. * In live mode, protection is offered only for a quote Post Coverage marks as live. * Store credit preview when the merchant's reward program is active, available to guests as well as account holders. The store credit is named as store credit: "earns $X in store credit", and with protection on the note "Credited to your account as store credit." * No offer, quote or fee on a cart with nothing to ship (virtual products only); a mixed cart is covered whole. Such an order is recorded as ineligible, and the settings screen says when the store ships nothing at all. * Each paid USD checkout order recorded, with no automatic retry of order creation and reconciliation by read-back. * Pricing changed after checkout: when Post Coverage refuses an order because its pricing changed, the plugin re-quotes it. If the fee is the one the customer paid, the order is recorded under the current pricing. If not, nothing is recorded and the sync screen offers "Record at current price" or "Check price again". * Lifecycle events delivered from a dedicated job table through Action Scheduler, with a sync screen and bulk retry. * "Issue a new claim link" order action. While the claim link email is on, the action names the billing address the new link goes to, and a second action issues the link without emailing it. Read a claim link with `PC_Orders::claim_link( $order )`. It returns no link while a claim link reset is being sent or has an unknown outcome. * "Package protection: claim link" WooCommerce email: the customer's private claim link, sent to the billing email address only (no Cc, Bcc or added recipients) when the order is recorded and when a new link is issued. Never the same code twice. On by default; switch it off under WooCommerce > Settings > Emails. * The claim link on the customer's own order in My Account. * "Email the claim link to " order action. * Actions for integrations: `post_coverage_woo_order_recorded`, fired with the order ID once an order is recorded with Post Coverage; `post_coverage_woo_claim_access_reissued`, fired with the order when a new claim link is issued; and `post_coverage_woo_record_event`, which a shipping integration calls to report a delivery event. * One request allowance shared with the Post Coverage Claim Lookup plugin, so the two plugins together stay inside the API's 40 requests per five minutes. * HPOS and Checkout block compatibility declared. == Upgrade Notice == = 1.4.1 = New heading font in the protection module (Inter Tight). No settings change. = 1.4.0 = Orders carry their products, digital items are left out of the protected amount, and refunding the protection fee ends protection on the order. No settings change. = 1.3.0 = No maximum order value: carts above $10,000.00 are now offered protection and recorded. No settings change. = 1.2.1 = Fixes the claim update email on stores where nobody opens the WordPress admin (set up with WP-CLI, or updated automatically): its 15-minute claim check is now scheduled by the plugin's background work. No settings change. = 1.2.0 = Adds the "Package protection: claim update" email: a short, content-free note to the customer when there is news on their claim, with their private claim link. On by default; switch it off under WooCommerce > Settings > Emails. News from more than a day before the update is not emailed. = 1.1.0 = Hides the protection module quietly, and spends almost no requests, while Post Coverage has switched the offer off for your store. No settings change. = 1.0.0 = First release. If the earlier "Post Coverage Sandbox" scaffold is installed, deactivate and delete it before activating this plugin.