Skip to content

Downloads ​

Stream the latest file of a product. What the endpoint asks for depends on the product's Download API Access setting.

Request ​

GET /api/index.php/v1/licensedock/downloads/{product_id}

Parameters ​

ParameterTypeInRequiredNotes
product_idintegerURLYesProduct ID
license_keystringQueryDepends on the settingThe license key
dlidstringQuery–Alias for license_key, sent by Joomla's updater. Also switches errors to HTML, see Errors
identifierstringQueryDepends on the settingAn activation identifier (domain, device, email, instance). Normalised by the plan's activation type

Example ​

bash
# License key
curl -fL -o release.zip \
  "https://yoursite.com/api/index.php/v1/licensedock/downloads/42?license_key=A1B2C3D4-E5F6A7B8-C9D0E1F2-A3B4C5D6"

# License key + activated identifier
curl -fL -o release.zip \
  "https://yoursite.com/api/index.php/v1/licensedock/downloads/42?license_key=A1B2C3D4-E5F6A7B8-C9D0E1F2-A3B4C5D6&identifier=example.com"

Use -f with curl. Without it, a refusal is written into release.zip and you end up with a "package" that is really a JSON error.

Download API Access ​

Set per product in Components → LicenseDock → Products → [product] → Details → Download API Access. It controls the API only. Emailed download links follow Settings → Delivery Method.

SettingWhat /downloads needs
Not setDecided from the product's own settings on every request – see below
License keyA valid key. Where it is used isn't checked
License key + activated identifierA valid key and an identifier already activated on that license
Account onlyNever served by the API. The customer downloads from their account
PublicNothing. Anyone can download

Not set resolves like this:

ProductBehaves as
Requires License = YesLicense key
Requires License = No, paidAccount only
Requires License = No, freePublic

A product counts as paid if it has ever had a price above zero, has ever sold for money, or is included in a paid bundle.

Those rules are also a floor. The setting can make a product stricter, never more open:

  • Public on a paid product still behaves as Account only.
  • License key or License key + activated identifier on a product with Requires License = No can't apply, because no key exists. The product behaves as Account only if paid, Public if free.

The product edit screen shows the mode currently in force under the field.

Sending an identifier ​

Sending identifier opts in to being checked against it, whatever the setting. An identifier that isn't activated is refused with ACTIVATION_NOT_FOUND, even when the product only asks for a key. Send one only after activating it.

Under Public, identifier is only logged.

The update check returns download_requires, so a client can find out what it needs before it tries.

Response ​

Success (200) ​

The file is streamed as an attachment:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="my-extension-2.1.0.zip"; filename*=UTF-8''my-extension-2.1.0.zip
Content-Length: 5242880
Cache-Control: no-cache, no-store, must-revalidate
X-Content-Type-Options: nosniff

The file served is the one the update check describes: the most recently uploaded published version, files ticked Include in auto-updates only, scoped to the license's plan. See Which Version Is Returned.

Errors ​

By default errors are JSON:

json
{
  "error": {
    "code": "LICENSE_EXPIRED",
    "message": "License has expired."
  }
}

When the request carries dlid, or an Accept header containing text/html, errors are an HTML page with the same HTTP status. It shows your store's brand name, the error code and message, what to do next, and a link to the customer's account. That covers a customer clicking a link and Joomla's updater, which sends dlid.

CodeHTTPWhen
DOWNLOAD_NOT_FOUND404Product doesn't exist or isn't published
INVALID_REQUEST400The setting needs a key and none was sent
ACCOUNT_DOWNLOAD_REQUIRED403The product is Account only
LICENSE_INVALID403Key doesn't exist, or its status isn't active
LICENSE_EXPIRED403License is past expires_at
PRODUCT_MISMATCH403The key doesn't cover this product, directly or through a bundle
ACTIVATION_REQUIRED403License key + activated identifier and no identifier was sent
ACTIVATION_NOT_FOUND403An identifier was sent and it isn't activated on this license
DOWNLOAD_NOT_FOUND404No published file for this license's plan
FILE_NOT_FOUND404The download record exists but the file is missing on the server
RATE_LIMITED429Too many requests. See below
INTERNAL_ERROR500Unexpected server failure

The endpoint stops at the first failure, in the order listed.

ACTIVATION_REQUIRED and ACTIVATION_NOT_FOUND are separate on purpose. The first means "send an identifier". The second means "activate the one you sent". A client can fix either itself.

Rate Limits ​

LimitScope
60 requests per 60 secondsPer IP
30 refused downloads per hourPer license key
20 downloads per hourPer key + identifier, when identifier is sent
20 per activation slot per hour, 400 at mostPer key, when no identifier is sent. Joomla's updater lands here

Logging ​

Every served download is logged with the user, product, license, file name, version, client IP and the normalised identifier. Every refusal is logged with the license, identifier and reason code.

View them in Components → LicenseDock → Downloads. Switch the filter to Refused for refusals, grouped by license and reason and kept for 90 days. Check it after tightening a product's Download API Access, to see which customers it stopped.

Security ​

  • Path containment – the served file must resolve inside the configured download directory
  • Forced binary type – files are always served as application/octet-stream with nosniff, so an uploaded SVG or HTML file can't run in the browser
  • No direct access – keep download files outside the web root, or block them in .htaccess / nginx. LicenseDock streams them itself

Joomla Extensions by Contona