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.

5 min readUpdated 3 August 2026downloads, digital, licence-keys, fulfilment

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:

  1. A signed-in session. Anonymous requests get 401. The call is rate-limited.

  2. 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.

  3. A completed order. Order must be completed before downloading. This is why a downloadable product that somehow sits PENDING is undownloadable.

  4. A downloadable product. Physical items are refused.

  5. 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.

Next: Shipping for physical goods, or Discounts.