What it is
APG Withdrawal for WooCommerce gives your shop the withdrawal form the law obliges you to have, and everything needed around it: the register of each declaration, the emails, the order status, the refund and the proof that you handled it as you should.
The customer goes to a page, chooses their order —or the specific lines they are withdrawing from, down to the unit—, reviews what they are about to send and confirms it. You get a notification, the request is stored with its date, its time and its fingerprint, and from there you accept it, reject it or complete it with the money returned.
Article 11a of Directive 2011/83/EU, introduced by Directive (EU) 2023/2673, has applied across the European Union since 19 June 2026: every online shop has to offer an online withdrawal function, prominent and easy to find. This is that.
It is a free plugin published in the official WordPress repository.
Requirements
- WordPress 6.0 or later.
- PHP 7.4 or later.
- WooCommerce 7.0 or later.
It works with the classic order store and with HPOS, high-performance order storage, without your touching anything.
Installation
- Search for APG Withdrawal for WooCommerce in Plugins -> Add New and press Install Now; or upload the ZIP from Plugins -> Add New -> Upload Plugin; or copy the folder into
/wp-content/plugins/over FTP. - Activate it from the Plugins menu.
- Set it up in WooCommerce -> Withdrawal.
On activation, the plugin creates the form’s page on its own with the [apg_withdrawal_form] shortcode inside it and leaves it selected in the settings. If you already have a page of your own, you point the setting at it and the spare one stays where it is: the plugin deletes nothing.
Important: WooCommerce 11.1 brings its own withdrawal feature, switched off as it comes, under WooCommerce -> Settings -> Advanced -> Features. With both running, your customers reach two different forms at two different addresses, each with its own register and its own acknowledgement, and neither knows about the other. The plugin detects it and warns you with a link to the screen where it is switched off.
The settings
Everything is on a single screen, in WooCommerce -> Withdrawal, laid out in blocks. That screen also has WordPress’s Help tab, at the top right, with one panel per block.
The shop and the form
Who receives the declarations and where the customer writes them. These details are also the ones that come out on the Annex I(B) model.
- Notification email: the address that receives a copy of each request. As it comes, the site administrator’s.
- Merchant phone (optional): shown next to the shop’s name, address and email on the printable model. Empty, that line does not come out.
- Withdrawal page: the page where the form’s shortcode is published. The plugin creates it on activation; here you point it at another one if you move it.
- Confirmation button text: what the final button says, the one that registers the declaration. As it comes, Confirm withdrawal.
- Jurisdiction: which rule is cited to the customer on the confirmation screen. Spain had not transposed Directive (EU) 2023/2673 when this was written, so both options apply the same thing today; the Spanish one also cites the TRLGDCU, the Spanish consumer act, and it is where the national extras will be switched on when they reach the official gazette.
The deadline and the register
How long the customer has and what is stored of each declaration. Fourteen days is the legal minimum in the European Union: you can give more, never less.
- Withdrawal window (days): once those days have passed, the order stops showing the withdrawal button. The form still accepts a request out of time, with its warning, because deciding whether it is out of time is for you and not for the form.
- Extra grace days: days of courtesy on top of the window, for the time a carrier takes or simply to be generous with your customer. As it comes, zero.
- Deadline source: which date it is counted from. Completed date, which is the closest thing to the delivery date, or the order date. With an order that is not completed yet, the plugin changes source on its own.
- Store IP address?: stores the address the declaration was sent from, as proof of who sent it and when. On as it comes.
- Store browser identifier?: stores the browser’s identifier, for the same reason. On as it comes. Both are personal data: keep them only if you are going to use them, and say so in your privacy policy.
- Delete all data on uninstall?: unticked, uninstalling the plugin leaves the requests and the settings where they are. Ticked, it takes the lot with it, and there is no coming back from that.
Handling the requests
What happens the moment a declaration arrives.
- Order status when a request is received: the plugin registers its own order status, Withdrawal requested, so that those orders are filtered, sorted and edited in bulk with WooCommerce’s usual tools, and any integration that reads order statuses —an ERP, a shipping plugin, the analytics— sees it. Leave it as it comes and the order is not touched.
- Account for the refund (IBAN): asks the customer for an account for when the money cannot go back the way it came. The number is checked with its international check digits (ISO 7064 MOD-97-10), stored alongside the request as banking data, included in the privacy exporter and removed entirely by the eraser. Ask for it only if you really need it. As it comes, it is not asked for.
- Confirm guest requests by email?: a visitor who has not signed in receives a single-use link at the address on the order, and the request does not count until they open that link. It keeps the date and time of the original submission, so nobody loses days of their deadline. Whoever is signed in is unaffected, because WordPress has already authenticated them.
Automation
A withdrawal has two parallel lives: the request, with its own status, and the WooCommerce order. Here the two are tied together so that you do not have to update both by hand.
There is one line for each request status —Accepted, Rejected and Completed—, and on each one you choose which order statuses move it. The usual thing is for a refunded order to leave its request as completed. It takes several statuses per line and, if you choose none, that line does nothing.
Emails to the customer
Which status changes write to the customer. As it comes, Rejected and Completed write and Accepted does not, because an accepted withdrawal is usually told with the money already returned.
The content of those emails is edited where all WooCommerce’s are, in WooCommerce -> Settings -> Emails: the plugin adds its three emails there as three more on the list, with their templates overridable from the theme.
Wording of the exclusion notices
What the customer is told when a product does not allow withdrawal. There is one field per type —Withdrawal excluded (generic), Digital content, Personalised product and Manual review required—, and that text comes out in two places: on the product page, between the price and the add to cart button, and again inside the withdrawal form.
The fields come filled in with the text the plugin brings, translated. Edit the one you want to say in your own way and empty the one you want back as it was.
Waiver for digital content
Downloadable content loses the right of withdrawal only if the customer accepted that before the download began, and the shop can prove it. That checkbox at checkout is the proof.
- When to show the checkbox: never, only on the products classified as digital content, or on every order. The middle one is the usual choice. As it comes, it is switched off.
- Custom checkbox text: comes filled in with the waiver’s wording. Edit it to say it in your own way, or empty the field to go back to the one the plugin brings.
Which products set it off is decided solely by the withdrawal type of the product and of its categories, never by the product being virtual or downloadable. The checkbox works just the same on the classic checkout and on the block one, and what the customer answers is stored in the order’s metadata with the exact text they saw, the timestamp in UTC, their IP, their browser and the kind of checkout screen.
What can be withdrawn from and what cannot
The law leaves some things outside the right of withdrawal: what is made to measure, what has been unsealed, digital content already downloaded. Each product carries its type on the Withdrawal tab of its data, and a category can set it for everything that hangs off it.
- Withdrawal allowed (default): the normal one, and what a product inherits while neither it nor its categories say otherwise.
- Withdrawal excluded: this product has no right of withdrawal, and the customer is told so before buying it.
- Digital content: the right is lost when the download starts, provided the customer accepted that beforehand. It is the type that sets off the waiver at checkout.
- Personalised product: made to the customer’s measure, so the law leaves it out.
- Manual review required: neither yes nor no. The request comes in and you decide. For the cases the law leaves to the state the goods come back in.
Below the type there is a Custom exclusion notice (optional) for writing that particular product a notice of its own, different from the general one for its type.
When a product is left on Withdrawal allowed (default), the type is inherited from its categories, and among several the most restrictive wins: a product in one ordinary category and in another marked as personalised comes out personalised.
The form the customer sees
The form goes in two steps, always: it is filled in, it is reviewed on a confirmation screen and only then is it sent. Nobody withdraws from an order by pressing a button by accident.
Whoever is signed in finds their details in place and chooses their order from a dropdown. Whoever comes as a guest writes the order number and the email address on that order, which is the pair the plugin validates before accepting anything —the same proof WooCommerce’s order tracking asks for—. The plugin never lists the orders of somebody who has only given an email address.
In Withdrawal scope you choose between the whole order or specific products. With the second, the list of the order’s lines appears, each with its box and its number of units, to withdraw from two of the five shirts and keep three. The quantities are trimmed to what the order really has.
If the order carries something excluded, that type’s notice comes out right there. If the deadline has passed, so does that, and the request can be sent anyway: it is marked as out of time for you to review.
The form is built to target WCAG 2.1 level AA, which is what EN 301 549 requires today, the standard the European Accessibility Act refers to, and it also meets WCAG 2.2’s target size criterion.
The Annex I(B) model
Below the form there is a link to the model withdrawal form of Annex I(B) of Directive 2011/83/EU, the one the law obliges you to make available to the customer. It opens as a printable page, with your shop’s details at the top and ruled gaps for writing by hand, and it is saved as a PDF with the browser itself. No library and no further plugin is needed.
Where to put the link to the form
The law asks for the link to be prominent and easy to find. Where to put it is for you to decide; the plugin gives you the tools:
- The page’s fixed address, the one in Withdrawal page.
- The
[apg_withdrawal_link]shortcode, with thelabel,classandtargetattributes, for a post, a page, a footer widget or an HTML block. - The equivalent block, for block themes and the site editor.
- The Withdrawal request action that appears on its own on every order that allows it, in My account -> Orders.
- The Withdrawal tab the plugin adds to the My account menu, with the form inside it.
- The processing order and completed order emails, where the link is injected on its own, except on the orders whose products are all excluded.
With no attributes, the shortcode writes exactly the sentence Article 11a suggests, so a bare [apg_withdrawal_link] already complies. If you give it an ambiguous label, the plugin warns you about it in the error log.
The requests
Every declaration your customers send is stored in WooCommerce -> Withdrawals, with its order, its date, the products it covers and what the customer wrote. It is the register you can show if you are ever asked to prove how you handled a withdrawal.
Above the list are the four statuses with their counters, which act as a filter:
- Pending: it has arrived and nobody has looked at it yet. It is the status every request is born in.
- Accepted: the withdrawal is valid. The refund comes from here.
- Rejected: out of time, excluded product or whatever it may be. Put the reason in writing, because the customer can ask you for it.
- Completed: all done, refund included.
The status column sorts, and the status is changed in bulk from the list’s actions dropdown, without going into each one.
The detail of a request
Inside each request is everything that was stored: the customer, their email address, their phone number, the order, the scope, the account for the refund if you asked for it, the deadline’s base date with its source, the time the email was confirmed if you use the guest confirmation, the IP and the browser.
Below are the selected products with their quantities, the detail the customer wrote, the acknowledgement that was sent to them and the status history: each change with its date, who made it and whether the matching email went out or failed.
Each request also stores a SHA-256 fingerprint of the acknowledgement, worked out over the data the customer saw and the date declared. It is what turns the register into evidence for the burden of proof of Article 16a(8): if anybody disputes what was sent and when, the fingerprint settles it.
The refund
Each request brings a Refund panel with what the declaration implies: the declared lines with their quantities and, when the withdrawal covers the whole contract, the cost of the standard delivery too, as Article 13(1) of Directive 2011/83/EU requires. All of it limited by what is left to refund on the order, so that a refund you already made by hand is not handed over twice.
Nothing happens until you press Create the refund. As it comes, the refund is only recorded in WooCommerce, without moving money or touching the stock. Two boxes change that:
- Send the money back through …: sends it through the payment gateway, with the gateway’s name written right there so that you know where the money is going. It only comes up when that gateway supports refunds; when it does not, the panel tells you so and lets you issue the payment from your bank.
- Restock the returned products: puts back the stock of what comes back.
It works with Redsys and Bizum without installing anything else, because the Redsys implementations for WooCommerce handle refunds through the standard refunds API and the panel uses that same API. Redsys supports full and partial refunds, so a partial withdrawal is refunded just as it was declared. When the customer sees the money depends on their bank, usually between 2 and 31 days: the 14-day deadline obliges you to issue the refund, it does not oblige your customer’s bank to credit it.
If the gateway refuses the refund, the reason it gives —the SIS code, in Redsys’s case— comes out in the error notice, so that you have something to act on.
The CSV export
The list’s Export CSV button downloads the whole register as a spreadsheet, with everything: the customer’s details, the order, the scope, the status, the out-of-time warning, the IP, the browser, the products, the refund account, the email confirmation time and the SHA-256 fingerprint.
It exports what you are looking at: with a status filtered, only that status. The file goes out in UTF-8 with its mark at the start, and the cells that begin with a formula character come out neutralised, so opening it in Excel, Numbers or LibreOffice runs nothing.
On the orders
WooCommerce’s order list gains a Withdrawal column with the status of each order’s request, and a filter of its own at the top to keep only the orders that have one. Inside the order, a side box links to its requests.
And there is the order status of its own, Withdrawal requested, which is placed right after Processing because it is a half-way status, not an end. It is applied only if you ask for it in the settings, it is available as a bulk action and the integrations that read order statuses see it.
Privacy and personal data
The plugin hooks into WordPress’s privacy tools, the ones under Tools -> Export Personal Data and Tools -> Erase Personal Data.
The exporter takes out all the requests for an email address with their data. The eraser does not delete them: it anonymises them. The personal data is replaced, and the request keeps its date, its status and its order, because that register is the evidence Article 16a(8) of the directive requires of you and deleting it would leave you without it. The order’s reference is kept for the same reason: it is a detail of the contract, not a personal identifier.
The plugin does not delete old requests on its own. As a general rule it is worth keeping them for at least 5 years, which is the usual limitation period for consumer claims across much of Europe, but check the one that applies in your country before throwing anything away.
The help inside the plugin
The settings screen and the requests list carry their own Help tab, at the top right, WordPress’s native one. In the settings there is one panel per block of the screen, and in the list one that explains the four statuses.
It is the short version of this page, without leaving the dashboard and in whatever language yours is set to. The side of that tab links here.
Frequently asked questions
For developers
The plugin has a REST API under the apg-withdrawal/v1 namespace, for headless shops and back-office integrations:
GET /requestslists the requests, withstatus,order_id,pageandper_page.GET /requests/<id>returns one.PUT /requests/<id>changes its status.POST /requestsregisters a declaration.
The first three ask for the manage_woocommerce capability. The fourth is public, just like the form, and goes through the same processor: the order has to match the email address, the attempt limiter is applied and, if you have it switched on, the guest confirmation comes in.
And it leaves these hooks:
apg_withdrawal_request_registered: fires with each registered request, with its ID and its data.apg_withdrawal_refund_plan: changes the refund’s figures before showing them, for example to deduct the depreciation of what comes back.apg_withdrawal_gateway_info: changes what the panel believes about the payment gateway.apg_withdrawal_excluding_types: which types count as excluded when deciding whether a whole order is.apg_withdrawal_show_order_link: decides whether the link to the form goes into that order’s email.apg_withdrawal_jurisdictionandapg_withdrawal_jurisdiction_supports: work the jurisdiction out yourself, for example according to the customer’s country in a multi-country shop.apg_withdrawal_guest_verification_requiredandapg_withdrawal_verification_ttl: when confirming by email is needed and how long the link is good for, which as it comes is one day.apg_withdrawal_rate_limit_maxandapg_withdrawal_rate_limit_window: the attempt limiter, which as it comes is five every ten minutes.apg_withdrawal_client_ipandapg_withdrawal_trusted_proxy_headers: where the customer’s IP is taken from behind a proxy.apg_withdrawal_account_title: the title of the My account tab. Return an empty string and none is drawn.apg_withdrawal_rest_request: the data the REST API returns for a request.
The requests are a content type of their own, apg_withdrawal, with the capabilities of WooCommerce’s orders and not the generic ones of posts: whoever does not administer the shop does not see them. A product’s exclusion notice has a shortcode of its own, [apg_withdrawal_notice], and its block, for the themes that rebuild the product page on their own.
The plugin is ready for WPML and Polylang, with its wpml-config.xml and the registration of the settings’ strings at run time, so the texts you write yourself are translated too.
Version history
Support
APG Withdrawal for WooCommerce is a free plugin and APG does not give free technical support. If you need help with the installation or the setup, there is our technical support service.
Bear in mind that this page is informative and it is not legal advice: how the right of withdrawal applies to your shop, what retention periods you keep and which products are left out is something you have to check with whoever handles your legal affairs.
For questions about what you read here, the comments below are open. And if you find it useful, you can make a donation or leave a review in the WordPress repository, which is what keeps the plugin alive.
Help
APG Withdrawal for WooCommerce is a completely free plugin for WooCommerce, so if you want to help APG keep it free, you can make a donation towards its improvement and maintenance.
Comments
