Updates & Downloads
Updates and packages are only served to installations that have proved entitlement on this request — never because a key was presented once.
POST /updates/check
{
"product_code": "MYCRM",
"license_key": "XXXX-XXXX-XXXX-XXXX-XXXX",
"current_version": "2.4.1",
"domain": "example.com",
"installation_id": "inst_8f72d91c4e6b0a35c7d2e19f4b8a06cd"
}
{
"success": true,
"update_available": true,
"current_version": "2.4.1",
"latest_version": "2.5.0",
"release_type": "stable",
"release_notes": "Faster reporting, 40% smaller export jobs.",
"published_at": "2026-09-28T09:00:00+00:00",
"download_available": true,
"changelog_url": "https://example.com/changelog/2.5.0",
"requirements": {
"minimum_php_version": "8.2",
"minimum_laravel_version": "11.0",
"requires_backup_acknowledgement": true,
"requires_full_download": true
},
"meta": { "request_id": "req_9f2c1a7b4d6e" }
}
Version comparison is numeric per segment, so 2.10.0 correctly outranks 2.9.0.
Release types
stable, beta, alpha, security, hotfix.
Pre-releases are never offered unless the product enables them or the request sets include_pre_releases: true.
Requirement gating
If the installation's PHP or Laravel version is below the release minimum, the response keeps update_available: true but sets download_available: false and explains why. An installation that cannot run the release is never handed one.
POST /downloads/request
{
"product_code": "MYCRM",
"license_key": "XXXX-XXXX-XXXX-XXXX-XXXX",
"version": "2.5.0",
"domain": "example.com",
"installation_id": "inst_8f72d91c4e6b0a35c7d2e19f4b8a06cd"
}
{
"success": true,
"url": "https://license.example.com/api/v1/downloads/3f9c...",
"expires_at": "2026-10-03T12:10:00+00:00",
"expires_in": 600,
"filename": "MYCRM-2.5.0.zip",
"version": "2.5.0",
"size_bytes": 18453220,
"checksum_algorithm": "sha256",
"checksum": "sha256:9f86d081884c7d65...",
"meta": { "request_id": "req_9f2c1a7b4d6e" }
}
How downloads are protected
Packages are stored on a private disk with serving disabled. There is no public URL for a .zip anywhere in the system.
- Entitlement is re-proved at grant time.
- A single-use token — 192 bits of CSPRNG output, carrying no data of its own — is recorded against the package, license and installation with a short TTL.
- The token is re-checked at redemption time: expiry, license status and installation status are all re-read. A license suspended between grant and download cannot fetch the bytes.
- The stream is chunked, so a large archive is never buffered in memory.
Each redemption writes a downloads row recording product, version, license, customer, installation, IP, user agent and bytes served.
Verifying the package
Always verify the checksum before extracting:
$expected = '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08';
$actual = hash_file('sha256', $downloadedPath);
if (! hash_equals($expected, $actual)) {
throw new RuntimeException('Package checksum mismatch.');
}
Safe updating
The server never overwrites a customer's files. For a one-click update flow:
- Back up — archive the current tree and the database.
- Verify — checksum the downloaded package.
- Stage — extract to a temporary directory, not over the live one.
- Swap — move the old tree aside, move the new one in.
- Roll back on failure — restore the previous directory and re-run the migration rollback.
requires_backup_acknowledgement is true by default so a UI can insist on step 1 before offering the update button.
Page 4 of 8