Updates & Downloads

v1 JSON over HTTPS Bearer credentials or HMAC signatures

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.

  1. Entitlement is re-proved at grant time.
  2. 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.
  3. 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.
  4. 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:

  1. Back up — archive the current tree and the database.
  2. Verify — checksum the downloaded package.
  3. Stage — extract to a temporary directory, not over the live one.
  4. Swap — move the old tree aside, move the new one in.
  5. 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