Selling with Commerce
Sell downloads, courses, memberships or anything else from your site, with hosted checkout, proper invoices, EU VAT, refunds and Peppol.
On this page
Commerce is a paid plugin from the store. It sells whatever you add as a product. Your buyers pay on a page hosted by the payment provider, so card details never reach your site, and you get your own numbered invoices, whichever provider you use. Your buyers become accounts with no admin access.
It runs in test mode from the moment you turn it on, with no licence and no account anywhere. The built-in sandbox stands in for a payment provider, with Pay and Decline buttons, so you can try a whole sale first.
#Sell something
- Under Commerce → Products, add a product: a name, an SKU and a price. Say whether the price is without VAT or is what everyone pays, and whether it's one-off or recurring.
- Say what it delivers. Attach a file and buyers get a download link for as long as their purchase stands. Name an access like
magazineand buyers can read gated content with that access. Both go away by themselves on a refund or a lost chargeback. - Put a Products or Buy Button block on any page. Both link to
/checkout?product=<sku>.
The buyer fills in who they are, sees the exact total with VAT, and then goes to the payment page. Afterwards the order page at /checkout/done says how it went, shows the invoice and the downloads, and a link to it goes by email. If the payment fails it says nothing was charged, and offers another try.
Customers with an account also find everything at /account/orders, /account/invoices, /account/downloads and /account/licenses.
You can make your own version of the checkout pages in the builder. They use the same blocks: Checkout Form, Order Review, Order Status, My Orders, My Invoices, My Downloads and My Licences. Make an entry in the Checkout Pages collection with the slug checkout, review or done. Order Status can say the outcome as the page's main heading, so that page needs none of its own. These blocks lay themselves out in two columns when they have the room.
#Test mode
Until a site goes live, every checkout page says it's in test mode, each invoice is marked as no tax document and uses the TEST- series, and each mail says no real money moved. If the seller's address is still empty, the pages say that real sales stay closed until it's filled in. A public site's test checkout is for the team only, so a stranger can't get what's on sale for free.
To try a business sale without anyone's real VAT number, use zeros and one of the EU's test codes: DE000000100 is valid, DE000000200 invalid and DE000000300 means the service is down. Only test mode sends these to the EU's test service, and a live site only asks the real register.
#Go live
Commerce → Go Live lists what stands between you and real money, and Go Live is refused until nothing does:
- a licence for Commerce on this site's public address, added on the Licence screen
- company details: legal name, address, country, email and, for EU VAT, a VAT number
- a tax regime, which you have to choose
- a payment provider with live keys
- when Buyers Outside the EU is set to the payment gateway's tax: a tax service that works, which for Stripe means a head office address in Stripe Tax
- working email, because invoices go out by email
- when you run a store: live signing keys, an
https://address and public accounts turned on
Nothing fills in your company details for you. You can type them under Settings → Commerce, or name a global there whose fields have the same names. A site where a required field later goes empty stops taking new orders until it's filled in, and raises an alert. Payments already made still settle.
The licence is checked only here, once. After a site has gone live, no failed, lapsed or missing licence ever stops it selling. The check isn't there to fail: nothing in the checkout reads the licence.
#Stripe
Choose Stripe under Settings → Commerce, and add your keys. A restricted key (rk_live_…) is best. With Stripe Tax, it also needs to write tax calculations and transactions and read tax settings. Then add a webhook endpoint in Stripe's dashboard, and copy its signing secret into the settings:
| Mode | Endpoint |
|---|---|
| Test | https://your-site.example/api/commerce/webhooks/stripe/test |
| Live | https://your-site.example/api/commerce/webhooks/stripe/live |
Send these events:
checkout.session.completed checkout.session.expired
checkout.session.async_payment_succeeded checkout.session.async_payment_failed
invoice.paid invoice.payment_failed invoice.payment_action_required invoice.voided invoice.upcoming
customer.subscription.updated customer.subscription.deleted customer.subscription.paused
refund.created refund.updated refund.failed charge.refunded
charge.dispute.created charge.dispute.updated charge.dispute.closedIn Stripe's settings, turn off its own emails for successful payments and refunds, since your invoices are the legal documents. Set failed subscription payments to cancel the subscription after the retries, and set Stripe Retry Window in the settings to the same number of days.
No webhook is lost for good: a sale also settles when the buyer returns, and missed events are picked up hourly. There's nothing to run on your server. Follow-up work happens after requests. If you do have cron, php cms commerce:work keeps things moving on a quiet site.
#Stripe Tax
Outside the EU, rates change by country, state and city, and what you collect depends on where you're registered. Stripe Tax keeps track of both. To use it:
- In Stripe, open Tax → Settings. Add your head office address, and set the preset product tax code to what you sell, such as software.
- Under Tax → Registrations, add each place you're registered outside the EU, such as the UK, Switzerland or a US state.
- Under Settings → Commerce, set Buyers Outside the EU to Tax From the Payment Gateway, Like Stripe Tax.
At checkout, Commerce asks Stripe Tax for the tax on the buyer's address and lines, and charges exactly that. It's the same figure on the order summary, the payment and the invoice, which names the tax and its rate as Stripe does: sales tax at 8.875%, GST at 10%. Where you aren't registered, the tax is zero and the sale goes ahead. A business that gives its tax number is reverse charged where that applies. Each paid sale is recorded in Stripe Tax under its order number, and each refund takes its share back, so Stripe's reports, filings and threshold monitoring cover every sale. Its monitoring points out where you may need to register next; your accountant decides.
Buyers in the EU are never sent to Stripe Tax: EU VAT stays as described below. Stripe charges for Stripe Tax per transaction where you're registered; see its pricing.
#VAT
With EU VAT chosen, every sale gets one of these:
| Buyer | VAT | On the invoice |
|---|---|---|
| Consumer in your country | Your rate | |
| Business in your country, with a VAT number | Your rate | |
| Business elsewhere in the EU, number valid in VIES | None | Reverse charge |
| Business elsewhere in the EU, no number or not valid | As a consumer there | |
| Consumer elsewhere in the EU, you're registered for OSS | Their country's rate | |
| Consumer elsewhere in the EU, not registered, under EUR 10,000 a year | Your rate | |
| Business outside the EU | None, or what Stripe Tax works out | Outside the scope of VAT, or reverse charge |
| Consumer outside the EU | Not sold, sold without tax, or what Stripe Tax works out |
Buyers Outside the EU decides the last two rows: Businesses Only, Without Tax, which is the default, Everyone, Without Tax, or Tax From the Payment Gateway. A gateway that can't work out tax sells as with the default.
Past the EUR 10,000 limit without OSS, sales to consumers in other EU countries stop and you get an alert, until you register and turn on Registered for OSS. Sales outside this site count too: add them under Cross-Border Sales Elsewhere.
If VIES is down at checkout, the buyer pays VAT and it's refunded by itself once the number checks out, with a credit note and a new invoice. Commerce → VAT gives you each quarter's figures: OSS per country and rate, domestic sales, the reverse-charge listing, sales outside scope and, under Tax Abroad, what Stripe Tax priced, per country, with a CSV download. Credit notes count in the quarter they're issued and name the quarter they correct.
Rates are in the plugin, per country and date. A seller outside the EU chooses No Tax, and another tax system is another TaxResolver; see Gateways and hooks.
This is software, not tax advice. Your accountant decides what applies to you.
#Invoices
Each paid order gets a numbered invoice, sent by email as a PDF. Numbers run per year with no gaps, and a test order uses a separate TEST- series that never touches them. Refunds get a credit note in a CN- series, at the rate of the original invoice. A document is never changed afterwards: to fix one, refund it or correct it, so a credit note and a new invoice are issued. Names and addresses go on the invoice, so the checkout asks for them in Latin letters.
#Refunds
Refund from an order in the admin: everything left, one line, or an amount. A refund of everything, or of a line, takes back what it gave. An amount is a goodwill credit, and the buyer keeps what they bought. The credit note goes out once the provider confirms. A refund you make in the provider's dashboard is handled the same way.
Consumers can withdraw themselves from the order page for 14 days, as the law gives them, unless they agreed at checkout to immediate delivery of a product that says so. Refund Window lengthens this, and extends it to businesses. A business that has had Refunds per Business, per Year gets sent to you instead.
A dispute changes nothing for the buyer while it's open, and it blocks refunds, since the bank has taken the money already. If it's lost, what the order gave is taken back and that buyer can't buy until you mark it settled. Whether the VAT comes back is for you to review, so no credit note goes out by itself.
#Subscriptions
A product with a recurring price sells as a subscription, on its own. The payment provider runs the schedule and the retries, so a renewal never depends on someone visiting your site. Each renewal gets its own invoice, with VAT worked out for that moment.
When a payment fails, the buyer keeps access through the retries and gets an email. If the retries run out, the subscription ends and the access goes. Buyers cancel from their order page and keep what they paid for until the term ends.
#Peppol
Belgian businesses have to receive structured invoices. Turn on Send E-Invoices over Peppol, and add your access point's keys: the plugin speaks to Recommand, and another provider takes one small class. For a Belgian buyer with a VAT number, the invoice then goes as Peppol BIS Billing 3.0 UBL, as well as the PDF.
If they aren't on the network, the checkout asks them to accept PDF by email, and records it. A refused document stays valid and raises an alert, and you can resend it after fixing it. One that may or may not have gone out is never sent again by itself. Change Peppol Country Pairs when another country's law asks for the same.
#Licences
Sell something that runs on someone else's site, and a licence comes with it. On a product, turn on Comes With a Licence and say what it licenses: the package name its key is checked against, how many months of updates it includes, and how many domains one site may have, for a site in several languages. A licence is one-off and per site, so it can't be sold by subscription.
The buyer finds it at /account/licenses. They add their site's domain there and get a key to paste into their site. Subdomains and subfolders are covered, www included, and local and staging addresses need no licence at all. What counts as one domain comes from the Public Suffix List, so client.example.co.uk falls under example.co.uk, while alice.github.io is a site of its own. The plugin carries a copy, which php tools/public-suffixes.php in the workspace refreshes. A site holds one key, so when the same person activates a second product on a domain they already use, it joins that licence and the site's next daily refresh brings it in. Everything else is self-service as well:
- Move a domain. The old address keeps working for 30 days, so a migration can overlap. Three moves in 90 days, then they ask you.
- Hand it over or sell it on. The recipient accepts from a link sent to their address, then the owner has 72 hours to object, in case their account was taken over. A handover keeps the old owner as a manager until the new one removes them. A resale clears the domains, unless the site went with it, and stops the seller's key.
- Extend updates. A separate product whose sale extends: time left is kept, and a late extension owes nothing for the gap.
- Replace the key, if it leaked.
A refund or a lost chargeback marks only that product's entry on the licence, and a chargeback that's won puts it back. A voluntary refund stops once a licence has been transferred; the law's 14 days don't. Whatever happens to a licence, the site keeps running: the most any state does is a banner in its admin.
Commerce → Licences shows who holds what. You can issue one by hand for a deal made elsewhere, send a notice, revoke after that notice runs out, or move a licence to someone who has proved they control the site, when its owner is gone. Someone who bought a domain from the old owner can prove it from their account, with a DNS record or a file, and the proof decides who holds it. A key only ever says what it covers: it holds no name and no address.
#Run a store
Turn on Run a Store under Settings → Commerce, and your site becomes a store: the /store shelf with search and filters, a page per listing, and the store protocol at /api/store/v1, which other sites read from Modules → Store. It sells anything. A plugin is installed from the buyer's admin, or with Composer. A file is downloaded from their account. To shape the shelf in the builder, use the Store and Store Item blocks, or the Store Items data source with a Repeat block: a page of yours at /store wins over the standard one, and is written again when the store changes. Its items have title, summary, url, image, category, cost, price, price_line, by, label and more; if you list them, add the Store Ranking block too. For your own item page, make a Checkout Pages entry with the slug store-item and bind to entry.listing: price, price_note, vat_line, buy_url, screenshots, description_html, versions, requirements, docs_url, by, seller and more.
Under Commerce → Store, add a listing, set a price and a price for more updates, and publish a version. A listing also has screenshots, what it needs to run and a link to its documentation:
php cms commerce:store-publish acme/forms dist/acme-forms-1.2.0.zip --notes="Fixes saving"Each version is signed, and a version number is never used twice. Going on sale needs a published version and a price. New prices are for new sales. Delisting stops new sales, while what's been sold keeps working and can still be extended. A free listing stays free once it's out. Block withdraws versions for a security reason: sites refuse them everywhere and warn their admins, and nothing installed is switched off.
A paid download goes out only if the licence is live right now and covers that site's domain and that version's release date. That's decided on your server, from your own records, so a refund shuts it at once, whatever an older key still says. A version marked as a security fix goes to anyone who ever held the package, for the domain their key names. Free Download for Local Sites lets anyone who says they're on a local address fetch a paid plugin, so it's off unless you turn it on.
#Keys
In test mode a store makes its own keys. Going live needs real ones, which sites will trust from then on. php cms commerce:store-keys init --mode=live makes them and shows you the root key once: it's kept nowhere, so store it offline. Or make them elsewhere and bring them in with import, so the root never touches the server. The two working keys stay on the server, encrypted with APP_KEY, because signing happens there. Publish the fingerprint, so people adding your store can compare it.
#Authors
Others publish in your store by invitation. Under Commerce → Store, invite someone. The link works once, for that address, for 14 days. They choose a vendor name, which all their packages start with, and work from /account/store: their listings and uploads. What an author lists is free. Only you sell, so nobody else is paid through your store.
Nothing an author does reaches a site without you. Each version waits for your review, where the scan lists what its code can do and points at lines worth reading. That's a filter, not a proof: a plugin runs with full access to a site, and your approval is what signs it. Suspending an author delists their listings, and copies already installed keep working.
Items on the shelf are ordered by downloads over the last 90 days, then the most recent release. A search matches the title first, then the summary and categories. Featured adds a label, and a page of yours can show featured items first. Nobody can pay for a higher place, and the shelf says so.
#Permissions
commerce.view sees orders and sales. commerce.manage adds products, refunds and going live. Customer data and money never reach the assistant or MCP.