Digital delivery
How downloadable products are actually delivered — the manual per-order attachment step, licence keys versus files, self-hosted versus external links, and every reason a paid download comes back empty.
A downloadable product is paid for and marked COMPLETED the instant the
buyer checks out. What it is not is delivered.
Nothing is attached to a product. Files and licence keys live on the order
item, not on the product row, and there is no rule, template or default that
copies anything onto a new purchase. Until an operator opens the order and
fills in the download options, the buyer's download panel says
"Nothing has been attached to this purchase yet" — on an order that has
already taken their money and already reads COMPLETED.
If you sell digital goods, watch the orders queue. This is the single most common support ticket on a new store.
Plan for it. On a low-volume catalogue it is a minute of work per sale. On a high-volume one, decide up front whether you can live with it.
The three delivery shapes
Open the order at /admin/ecommerce/order/<id>, find the download options
panel, and pick one:
| Option | What the buyer receives |
|---|---|
| Licence key only | A key string they can copy. No file |
| Downloadable file only | A download link. No key |
| Both | Key and link together |
An optional instructions field is shown to the buyer alongside whichever of those you chose — activation steps, a support address, a version note.
Whichever fields the selected option does not use are cleared. Switching an item from Both to Licence key only deletes the attached file path. That is intentional — the alternative left a superseded file being served after you thought you had removed it — but it means editing an existing delivery requires re-entering everything you want to keep.
The attachment is scoped to the order in the URL: an order item id that belongs to a different order is rejected, so an operator cannot attach a key to somebody else's purchase by pasting the wrong id.
Files: your box, or somebody else's
The download link field accepts two different things and behaves completely differently for each.
An external URL
Anything starting http:// or https:// is handed to the buyer exactly as
written. Your S3 bucket, your CDN, a signed link from another service — the
platform does not fetch, proxy or validate it.
That means the URL is the access control. Once a customer has it, it works for anyone they send it to, for as long as it is live. Use pre-signed, short-lived URLs if that matters, and re-issue them per order.
A path on this server
Anything else is treated as a path inside the platform's uploads root. The
buyer never receives that path. They get
/api/ecommerce/download/<orderItemId>/file, which re-checks their session,
their ownership of the order and the order's status on every fetch, then
streams the bytes as an attachment with Cache-Control: private, no-store.
This is the option to prefer. A link that leaks is useless to whoever receives it.
Paths are stored as web paths — /uploads/ecommerce/products/manual.pdf — and
resolved inside the uploads root. Two failures are reported distinctly so you
can tell them apart:
- 403 — the path escapes the uploads root. Something is wrong with what was typed.
- 404 — the path is inside the root but nothing is there. The file was moved or deleted.
The uploads root is frontend/public/uploads — in production relative to the
deployment root, in development one level up from the backend's working
directory. Override it with UPLOAD_DIR if your layout differs. Getting this
wrong is what makes every download 403 at once.
What you are allowed to upload
The platform's upload endpoint accepts a deliberately inert set of formats for deliverable goods, at up to 10 MB per file:
| Documents | Archives | Audio | Text |
|---|---|---|---|
.pdf, .epub |
.zip, .7z, .gz, .tar, .rar |
.mp3, .wav, .ogg, .flac |
.txt, .csv |
HTML, SVG, JavaScript and XML are refused. They render or execute inline from a public directory, which would turn your store into a stored-XSS delivery mechanism.
Anything outside that list — a large video, a 400 MB game build, an installer — has to be hosted elsewhere and delivered as an external URL. There is no upload control in the download options panel in any case: you paste a path or a URL that already exists.
What the buyer's side checks
GET /api/ecommerce/download/<orderItemId> is the metadata call. In order, it
requires:
-
A signed-in session. Anonymous requests get 401. The call is rate-limited.
-
Ownership. The order item must belong to an order owned by the caller. Anything else is a 404 — not a 403, so probing ids tells an attacker nothing.
-
A completed order.
Order must be completed before downloading. This is why a downloadable product that somehow sitsPENDINGis undownloadable. -
A downloadable product. Physical items are refused.
-
Something attached. Neither a key nor a file path means
Nothing has been attached to this purchase yet.
It returns the download URL, file name, file size, the licence key and the
instructions. It also returns expiresAt, 24 hours out.
The value is displayed so the buyer treats the link as short-lived, but the file route re-derives access from the session and the order on every request. There is no expiry check and no download counter. Access lasts as long as the order does.
Deactivating a product does not revoke downloads. That is deliberate: someone who paid keeps what they paid for, whether or not you still sell it. The only way to actually revoke access is to cancel the order — which refunds them.
Practical patterns
Same file for every buyer. Upload it once, then paste the same
/uploads/... path onto each order. Tedious, but the file route still enforces
per-customer access, so one path is not a shared link.
Unique key per buyer. Generate the key in whatever system issues it, paste it into the licence key field, and use the instructions field for activation steps. The buyer's order page gives them a copy button.
Both. The common shape for software: the installer as a file (or an external URL, if it is over 10 MB) plus a per-customer key.
Nothing to deliver yet — a pre-order, or a service. Leave the item unattached and use the order status and email to communicate. The buyer sees the "nothing attached" message, so tell them what to expect in the product description.
Auditing what you delivered
Every attachment goes through one endpoint under the ADMIN_ECOM log module
with the title Add order download details, and it needs the
edit.ecommerce.order permission — the same permission that refunds money.
That is not accidental: changing what a buyer receives after they have paid is
a financial action, and it is recorded like one.
Downloads themselves are logged under the ECOM module and rate-limited.
There is no per-item download count in the product, so if you need "how many
times did they fetch it", the request log is where it lives.