EU Directive 2023/2673, Article 11a

A withdrawal button for WooCommerce, with a timestamped record of every declaration

Since 19 June 2026, EU shops that sell online must give customers an electronic way to withdraw from a purchase. This plugin adds that function to WooCommerce: a two step form that works with or without a customer account, and an email acknowledgement with the date and time of receipt that the customer can keep.

Free under the GPL and published on WordPress.org. FDesign charges for setup, wording review and ongoing care, never for the plugin itself.

What the law asks for

What Article 11a requires from a withdrawal function

The article covers every contract concluded through a website or an app. It sets four requirements, and one rule about timing: a withdrawal counts as made in time if the customer submits it before the withdrawal period ends.

01

A clearly labelled function, always available

The function must carry the words “withdraw from contract here” or an equally clear phrase. It has to be easy to find on the shop and stay available for the whole withdrawal period. A general contact form or an email address is not the same thing.

02

A short online statement

The customer gives or confirms three things: their name, the contract they are withdrawing from, and the email address where the confirmation should be sent. The article asks for nothing more.

03

A separate confirmation step

The completed statement is sent with a second button, labelled “confirm withdrawal” or an equally clear phrase. Opening the withdrawal link on its own does not send anything.

04

An acknowledgement on a durable medium

Once the customer confirms, the shop must send, without undue delay, an acknowledgement of receipt that the customer can keep, such as an email. It has to state what was declared and the date and time it was submitted.

The stake

What happens when the function or the information about it is missing

Each member state sets its own fines, and they vary. The consequences below come from EU law itself and apply in every member state.

Fourteen days can become twelve months EU wide

Article 6(1)(h) of the Consumer Rights Directive now requires the shop to tell customers, before they buy, that the withdrawal function exists and where to find it. When that information is missing, Article 10 extends the withdrawal period by twelve months. If the shop supplies the information later, the period ends fourteen days after the customer receives it.

Each side has something to prove

Under Article 11(4), the customer has to prove that they withdrew, and the timestamped acknowledgement is that proof. Under Article 6(9), the shop has to prove that it gave the required information, including where the withdrawal function is. A clear record helps both sides.

In force since 19 June 2026

Member states apply these rules from 19 June 2026. A shop that sells online to EU consumers without the function is already exposed to the longer withdrawal period.

The terms and returns pages need updating too

Installing the function is half of the work. The information shown to customers before they buy, the terms and the returns page must also say that the function exists and where it is. The plugin provides the function; the wording on those pages belongs to the shop.

Section by section

How the plugin works, step by step

Each part below describes what the plugin does in a live WooCommerce shop. All of it has been tested on a clean WordPress and WooCommerce installation, in both of the ways WooCommerce can store orders.

Where the link appears

Customers find the link wherever they look

On activation, the plugin creates a page called “Withdraw from contract” that holds the withdrawal form. Customers can reach it from several places, and the shop decides which ones to use under WooCommerce, Settings, Withdrawal.

  • In the footer of every page. On by default. This keeps the function visible for the whole withdrawal period, as the article requires.
  • In My Account. A link next to each order in the customer’s order list, shown only on orders that are still inside the withdrawal window and still have something left to withdraw. Also on by default.
  • Anywhere else. The [fdwb_button] shortcode places the same link on any page, for example a returns page.
  • One label everywhere. The link reads “Withdraw from contract” unless the shop sets its own wording in the settings.

Three ways in

For customers with an account, for guests, and for everyone else

The same form serves three different situations, so no customer is left without a way to withdraw.

  • Signed in. From the order in My Account, straight into the form. The plugin checks that the order belongs to the customer who is signed in.
  • Guest. The customer enters the order number and the email address used for the order. When both match, the plugin emails a link to that address. The link stays valid for 30 minutes and opens the form for that order only. The page shows the same message whether anything matched or not, so the form cannot be used to find out whether an order exists.
  • Free text declaration. For a customer whose link expired, who no longer has access to that inbox, or who cannot find the order number. They give their name, a contact email and enough detail to identify the order. The declaration is recorded as unverified and marked for the shop to match by hand, and the customer still receives an acknowledgement.

Two steps

Declaring is a deliberate act, not a stray click

The form follows the structure of Article 11a. The customer first sees exactly what they are about to declare, then confirms it as a separate action.

  • Step one shows the order and lets the customer choose what to withdraw. A reason can be given but is never required.
  • Step two repeats the choice and asks for confirmation with a button labelled “Confirm withdrawal”. Nothing is sent before that.
  • A double click cannot file two declarations. The first submission is recorded, and the second one lands on the same confirmation page.

Partial withdrawal

Per item and per quantity, counting refunds already made

Customers often withdraw from part of an order rather than all of it. The plugin offers only what is genuinely left to withdraw.

  • The customer chooses which items, and how many of each.
  • Refunds already issued reduce what is left. If a customer bought three, withdrew one and was refunded for another through WooCommerce, the form offers one, not two.
  • Items already withdrawn, items in excluded categories and the individual parts of a product bundle are not offered.
  • Partial withdrawal is on by default and can be switched off, in which case a customer withdraws from the whole order or not at all.

The acknowledgement

The email the article asks for

As soon as the customer confirms, the plugin sends an acknowledgement of receipt. It shows the reference, the date and time of receipt, the order, exactly what was declared, and an integrity code.

  • The integrity code is calculated from the content of the declaration with a key that only the site holds. If the stored record were ever changed, it would no longer match the code in the customer’s email.
  • The wording says received, never accepted. Receipt and acceptance are separate legal steps, and the email keeps them apart.
  • It goes to the email address of the order, or to the address the customer gave in a free text declaration. Plain text format is available and suits a document the customer may want to keep or print.

The request list

Where the shop handles each request

All requests appear in one list under WooCommerce, with the number of pending requests shown next to the menu item so none are missed.

  • Filter by status, and search by reference, customer name, email or order number.
  • Every request keeps a full history: who did what, and when.
  • Accept or reject with a note that goes to the customer. The note is optional when accepting. When rejecting, a reason is worth the thirty seconds it takes, because a refusal without one cannot be answered by the customer or defended later by the shop.
  • Each request also adds a note to the order, and the order screen shows a panel with all of that order’s withdrawal requests.
  • A declaration recorded outside the withdrawal window is marked as such in the order note, so the shop can see it at once and decide.

Orders and refunds

The plugin records declarations; refunds stay in WooCommerce

The plugin never moves money. The shop issues refunds with WooCommerce’s own refund controls on the order, through its usual payment gateway, after inspecting the returned goods.

  • No monetary amount is stored with a request. What the shop owes depends on delivery costs, discounts spread across several items and the condition of the goods, so a figure fixed at the time of the declaration would look official and be wrong.
  • Two optional order statuses, “Withdrawal requested” and “Withdrawn”, are off by default. When they are switched on, the previous status is remembered and restored if a request is rejected, and a partially withdrawn order returns to its previous status, because the rest of the order still stands.

Exclusions

Products that carry no right of withdrawal

Some goods are excluded by law from the start, such as items made to the customer’s specification or goods that spoil quickly. The shop can exclude them under WooCommerce, Settings, Withdrawal, Exclusions, and they are then never offered in the form.

  • Exclude whole product categories.
  • Exclude virtual or downloadable products, each with its own switch. Both are off by default, because a service or digital content loses the right of withdrawal only under strict conditions: the customer agreed in advance to an immediate start and acknowledged losing the right, and for digital content the shop confirmed this to them. These switches belong on only if the checkout collects that consent.
  • Sealed health, hygiene and cosmetic products are not excluded automatically. They lose the right only once opened, and no software can tell whether a package was opened. That check belongs to the team receiving the return.

Five emails

The customer is told what happened, in the shop’s own words

Access link, acknowledgement of receipt, notification to the shop, acceptance and rejection. All five appear under WooCommerce, Settings, Emails, next to the shop’s other emails.

  • Every one has WooCommerce’s standard Additional content box.
  • The three that the customer reads also let the shop replace the closing paragraph, the one that explains what happens next. Clearing the field brings back the built in wording, in the customer’s language.
  • The default acceptance text says that the shop will write separately about returning the goods and the refund. That is true of most shops but not all, so a shop with a different process should describe its own.
  • The notification goes to the addresses set under Withdrawal, Notifications, or to the site administrator if none are set.

Deletion

Records can be removed, but never without a trace

Spam, duplicates and the test entries left behind during setup should be removable. A genuine declaration is different: it records that a customer used a legal right on a given date, and it is evidence for the shop as much as for the customer.

  • Before a request is deleted, the whole record is written to the WooCommerce log, together with the reason given and the user who deleted it.
  • The order receives a note with the reference, the time the declaration was received and the user.
  • Neither can be deleted from inside the plugin, and there is deliberately no bulk delete.
  • Uninstalling the plugin keeps all records unless the shop chooses otherwise under Withdrawal, Advanced.

Privacy

GDPR tools, with erasure off by default

Withdrawal requests appear in WordPress’s Export Personal Data and Erase Personal Data tools, and the plugin adds suggested text to the privacy policy draft.

  • Erasure is off by default, and the erasure report says so instead of failing silently. A withdrawal declaration records a legal act and is the shop’s evidence that it was honoured, which is a recognised reason to keep it. WooCommerce treats orders the same way.
  • When erasure is switched on, the name, address and free text are replaced while the declaration itself remains, so the proof is kept without keeping the personal data.
  • Visitor IP addresses are used only for rate limiting, kept as a salted hash in the plugin’s own table, and never stored with a declaration.

Engineering

Security and reliability details

A withdrawal function can stop working without anyone noticing. These are the risks that would stay hidden, and how the plugin handles each one.

Access links are stored only as hashes 30 min

Each link contains 32 random bytes from a cryptographic source. The database keeps only a SHA-256 hash of it, each link works for one purpose only, and its expiry is part of the database query itself. A leaked database backup contains nothing that opens a withdrawal form.

The link leaves the address bar

When the customer opens the emailed link, the plugin swaps it for a secure cookie that works only on the withdrawal page, then reloads a clean address. The link therefore does not stay in the browser history or travel to other websites the page loads files from.

Every form carries a security token

All forms include a WordPress nonce that is checked before anything is processed. A form submitted from another website is refused, and nothing is recorded or sent.

The withdrawal page is never cached

The page excludes itself from caching with the signal that WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache and WP Fastest Cache all respect, on every translation of the page. A cached copy could show one customer another customer’s session. If the form appears somewhere the exclusion does not cover, the WooCommerce log says so.

The lookup cannot be used to search for orders

A hidden field catches automated submissions, and a signed time stamp must be at least three seconds and at most one day old. A wrong order number and a wrong email get exactly the same answer as a correct pair, so the form reveals nothing about which orders exist.

Rate limits hold on a busy site

At most ten lookups per visitor in ten minutes, five access links per email address per hour, five confirmed withdrawals per visitor per hour and three free text declarations per visitor per hour. The counts are kept in the plugin’s own table rather than in WordPress transients, which can be cleared under memory pressure and would let the limits fail exactly when the site is busiest.

Both ways of storing orders

WooCommerce keeps orders either in its newer dedicated tables (HPOS) or in the older posts table. The plugin was tested on both, including order lookup by the custom order numbers that renumbering plugins create.

0

errors in
Plugin Check

0

PHPStan level 5
findings

114

attack checks,
all passing

2

order storage
modes tested

3

ways into
the form

Alternatives

How it compares, checked in their code

On 29 August 2026, six comparable plugins were listed on WordPress.org, all of them free. The table below compares this plugin with those six, based on reading their code on that date rather than their descriptions. More plugins have appeared since, so the table is a record of that date.

  FDesign Vendidero WebToffee Four others
Active installsnew5,0001,00050 to 500
Guest path without an accountyesyesyesvaries
Access links stored as hashesyesyesnovaries
Rate limiting on the lookupyesnonono
Refunds reduce what is leftyesnonono
Page cache exclusionyesnonono
Reason attached to a rejectionyesyesyes, requiredno
Shop can reword the emailsclosing paragraph toostandard blocknotwo of four
Greek interfacecompletenopartialno

Install counts are WordPress.org’s own figures, read on 29 August 2026. Vendidero is the most widely used and the most complete of the six, and where it does something better, this plugin adopted it. The rejection reason came from WebToffee, which does not let an administrator reject a request without typing one.

Security

How the security tests were chosen

Reviewing a plugin against a remembered list of vulnerabilities means reviewing it against the ones that happen to come to mind. The list for this plugin came from the National Vulnerability Database instead.

2,257 WordPress plugin vulnerabilities

Every one published between 1 May and 29 August 2026, sorted twice: by how often each type appears, and by how serious it is. The two orders turned out almost opposite.

151 of the 156 critical ones need no login

Cross site scripting is the most common type, with 545 records, and appears zero times among the critical ones. Unrestricted file upload is eighth by count and first by severity.

A guest link works like password recovery

Weak password recovery is nineteenth by count and seventh among the critical. An emailed access link works the same way in practice, so every classic weakness of a recovery link was tested for.

What that produced

Two attack suites with 114 checks between them, plus two end to end tests over HTTP, all run on a clean WordPress and WooCommerce installation with debugging switched on, in both order storage modes. The emailed link is built from the site’s own address and never from information in the incoming request, so it cannot be pointed at another server. Expiry is enforced inside the database query. Fifty issued links are fifty different values, and the raw link is stored nowhere in the database.

Verified against NIST National Vulnerability Database, WordPress Plugin Check, PHP_CodeSniffer with the WordPress standards, and PHPStan at level 5. Vulnerability figures read on 29 August 2026. Tests last run on 3 October 2026 with WordPress 7.1.2 and WooCommerce 11.1.2.

Languages

Translations through WordPress.org

How translations arrive

Language packs, installed automatically by WordPress

The plugin is written in English and has 302 translatable texts. Translations are managed on translate.wordpress.org, in the same way as WordPress itself, and WordPress installs the right language pack for each site automatically.

  • Greek is complete. Every text, including the admin screens and the legal wording, was reviewed line by line by a fluent speaker and submitted to translate.wordpress.org. Greek language pack is available from translate.wordpress.org.
  • Other languages are prepared but not published. Drafts exist for German, French, Spanish, Italian, Czech and Romanian. WordPress.org accepts only translations reviewed by a fluent speaker, so each one waits for that review.
  • Anyone can help. A shop or translator can contribute or correct translations on the plugin’s page at translate.wordpress.org.
  • Works with Loco Translate, WPML and Polylang. On a multilingual shop, the withdrawal link leads to the page in the visitor’s language, and the cache exclusion covers every translation of the page.

The plugin

Free under the GPL, with no paid edition

There is no separate premium version. The plugin on WordPress.org is the one this page describes, and FDesign does not sell a licence for it.

What FDesign offers is the work a plugin cannot do on its own: choosing the withdrawal window and the order status that starts it, adding the required information to the terms and returns pages, checking the email wording against how the shop handles returns, and support when a customer’s declaration arrives.

Download

FDesign Withdrawal Button for WooCommerce

Install it from Plugins, Add New in WordPress by searching for its name, or download the zip from WordPress.org and upload it.

Version 1.0.1 Licence GPL-2.0 or later Requires WordPress 6.9, PHP 7.4 WooCommerce 8.0 or later, tested with 11.1, HPOS compatible Sends data to no external server
Get it on WordPress.org

Services

Setup and ongoing care by FDesign

Two kinds of service. A one off setup puts the function live on the shop and tests it there. Monthly care keeps it working as WordPress, WooCommerce and the shop’s own catalogue change.

One off

Compliance setup

One shop

from €140

A single WooCommerce shop, live and tested.

  • Installation, plus the withdrawal window and the order status that starts it
  • Withdrawal page created, linked and excluded from caching
  • Guest path tested end to end on the shop’s own staging site
  • Email wording adapted to how the shop handles returns
  • Exclusions reviewed against Article 16
Ask about setup

Larger or unusual

on assessment

Multisite, marketplaces, or a heavily customised checkout.

  • Multisite and multi vendor setups
  • Non standard order numbers or external fulfilment
  • Custom templates matched to the theme
  • Migration from another withdrawal plugin
Ask for an assessment

Monthly

Compliance care

One shop

from €29 / month

Keeps the function working while everything around it changes.

  • Plugin, WooCommerce and WordPress updates applied and checked
  • The guest path tested again after every major WooCommerce release
  • The request list watched so nothing waits unanswered
  • Email delivery monitored
Ask about care

Larger or unusual

on assessment

Many shops, or obligations that reach beyond WooCommerce.

  • More than five shops
  • Agreed response times
  • Reports that a compliance team can file
Ask for an assessment

Prices exclude VAT. Greek VAT of 24% is added where it applies.
The plugin itself is free and stays free. These prices cover the work around it.

Who built it

FDesign

Dorothy Fiedor

Dorothy Fiedor

Founder of FDesign, Senior WordPress and WooCommerce Engineer

Dorothy Fiedor is a senior WordPress and WooCommerce engineer in Athens and the founder of FDesign. She designs, builds and runs production stores end to end, from the UX and the custom plugins to WooCommerce HPOS migrations, ERP and marketplace integrations, and the managed hosting behind them, including PHP-FPM worker tuning and MariaDB upgrades. Since 2020 she has also been the in house WordPress engineer for a supplement ecommerce group that runs several brands.

Her background is in design. She trained in graphic design in Athens and at the Academy of Fine Arts in Katowice, produced multilingual publications for EU institutions, and moved to the web in 2015, building WordPress sites, hand coded HTML email for major brands such as Papastratos and BMW, and Magento front ends for car parts retailers, while growing FDesign alongside. That route from design into engineering is why her work covers the interface, the code and the infrastructure as one job.

BA in Graphic Design, Technological Educational Institute of Athens. Polish native speaker, fluent in Greek and English.

WordPress and WooCommerce WooCommerce HPOS WordPress Coding Standards Plugin Check PHP and MariaDB PHP-FPM Managed hosting and Plesk Cloudflare WPML JSON-LD structured data GA4 and Tag Manager Python and REST APIs Figma and Adobe CC

Questions

Before you install it

Is the plugin really free, with nothing held back?

Yes. It is published on WordPress.org under the GPL, and there is no premium edition with extra features. FDesign earns from setup, wording review and monthly care, which are the parts a plugin cannot do for a shop. Nothing in the plugin is limited in order to sell an upgrade.

Does installing it make my shop compliant?

Not on its own. The plugin provides the withdrawal function and the timestamped acknowledgement. The shop still has to state in the information shown before purchase, in its terms and on its returns page that the function exists and where to find it, as Article 6(1)(h) requires, and decide how returns are handled when they arrive. Check both against the law as it applies in your country.

Where does the withdrawal link appear?

On activation the plugin creates a page called Withdraw from contract with the form on it. A link to that page appears in the footer of every page and next to each eligible order in My Account, and both can be switched off in the settings. The [fdwb_button] shortcode places the same link anywhere else, for example on a returns page.

Can I label the link Cancel or Returns instead?

Not safely. Article 11a asks for the words withdraw from contract here or an equally clear phrase. A label that customers do not recognise as the withdrawal function is unlikely to meet the requirement, and Cancel reads as cancelling an order that has not shipped yet. The default label is Withdraw from contract, and the wording can be changed in the settings.

What happens when the withdrawal window has closed?

The link in My Account disappears, and the guest form no longer sends an access link for that order. A customer can still file a free text declaration: it is recorded, the customer receives an acknowledgement, and the shop decides after matching it to the order. When a declaration is recorded outside the window, the note on the order says so. Under Article 11a, a withdrawal counts as made in time if it was submitted before the period ended, so the time of receipt in the record is what matters.

Does it issue refunds?

No, deliberately. Refunds stay in the normal WooCommerce flow, through the shop’s usual payment gateway, and the shop decides on them after inspecting the returned goods. The plugin also stores no monetary amount, because what a shop owes depends on delivery costs, discounts spread across several items and the condition of the goods. A figure fixed at the time of the declaration would look official and be wrong.

What about sealed hygiene or cosmetic products?

Sealed health, hygiene and cosmetic products lose the right of withdrawal only once the customer has opened them, so an unopened item can still be withdrawn. For that reason the plugin does not exclude such products automatically: no software can tell whether a package has been opened, and that check belongs to the team receiving the return. Products that never carry the right, such as goods made to order, can be excluded by category.

Does it work behind Cloudflare and with a caching plugin?

Yes. The withdrawal page excludes itself from caching with the standard signal that WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache and WP Fastest Cache all respect, on every translation of the page. Behind a CDN such as Cloudflare, switch on Behind a CDN or reverse proxy under WooCommerce, Settings, Withdrawal, Advanced. Without it, every visitor appears to come from the same address and shares one rate limit. If the CDN uses a cache everything rule, add a bypass for the withdrawal page.

A customer asked to be deleted. What happens to their withdrawal?

By default it is kept, and the erasure report says so and why. The declaration records a legal act and is the shop’s evidence that it was honoured. A shop that prefers otherwise can switch on Erasure requests under WooCommerce, Settings, Withdrawal, Advanced. The name, address and free text are then replaced with anonymised values, while the declaration, its reference and its date remain.

Add the withdrawal function to your shop

Install the plugin from WordPress.org, or have FDesign install it, adapt the wording to the shop’s returns process and test the guest path on a staging site before real customers use it.

Every setup includes the guest path tested end to end on the shop’s staging site before it goes live.