Software Updates & Client Integration

Practical workflow guide for bootstrapping clients, requesting purpose-aware software packages, and verifying package integrity

Overview

This guide explains how to integrate client software—such as WordPress plugins, themes, desktop utilities, or CLI applications—with WooNooW's Software Distribution engine.

While the Software Distribution API serves as the normative REST protocol specification, this guide focuses on practical client workflows: establishing persistent identity, requesting purpose-aware packages, handling short-lived download tokens, verifying byte integrity, and displaying entitlement status in your application's user interface.

If you are a store administrator or release manager configuring vaults and products, see the Software Distribution Configuration Guide.


1. Client Bootstrap & Identity

Every protected client installation must establish a persistent identity before interacting with WooNooW APIs. Identity is defined as a two-part pair:

text
persistent installation UUID + normalized domain

Neither component can replace the other. A license key alone is never sufficient to authorize a download.

sequenceDiagram
    participant App as Client Software
    participant Storage as Local Storage
    participant Lic as Licensing API
    participant Dist as Software API

    App->>Storage: Read or generate UUIDv4
    App->>Lic: POST /licenses/activate (UUID + domain + key)
    Lic-->>App: Activation confirmed (activation_id)
    App->>Dist: POST /software/package (purpose, identity, key)
    Dist-->>App: Package metadata + short-lived download_url

Bootstrap Workflow

  1. 1. Generate and persist a canonical UUID

    On first launch, generate a random UUIDv4 (36 characters, lowercase hexadecimal with hyphens). Persist this identifier permanently in the client's local configuration (for example, in WordPress options, a local database, or a protected file). Never regenerate the UUID across updates or domain migrations. Read Website Identity for detailed lifecycle rules.

  2. 2. Normalize the current domain

    Obtain the current site URL or hostname. WooNooW normalizes it to a canonical host by removing the scheme, path, query, fragment, and default ports (80/443), while preserving non-default ports.

  3. 3. Activate the installation

    Activate the license key against the combined identity using the Licensing API:

    http
    POST /wp-json/woonoow/v1/licenses/activate
    Content-Type: application/json
    
    {
      "license_key": "XXXX-YYYY-ZZZZ-WWWW",
      "domain": "https://customer-site.example",
      "installation_id": "550e8400-e29b-41d4-a716-446655440000"
    }
    

    Subsequent activation calls with the exact same identity are idempotent and do not consume additional activation slots.

  4. 4. Request software packages

    Once activated, use the canonical POST /software/package endpoint to retrieve install, reinstall, or update packages.


2. Canonical Package Flow (POST /software/package)

The canonical way to request software packages is POST /software/package. This endpoint is purpose-aware and selects the authorized release on the server.

http
POST /wp-json/woonoow/v1/software/package
Content-Type: application/json

Purpose Semantics & Parameters

The request body accepts only these parameters; any unknown fields are rejected with 400 unsupported_parameter:

ParameterTypeRequiredPurpose RulesDescription
slugstringYesAllUnique software slug configured on the parent WooCommerce product.
purposestringYesAllMust be exactly install, reinstall, or update.
current_versionstringConditionalRequired for update.
Must be omitted for install and reinstall.
The actual currently installed software version.
license_keystringProtected productsAllLicense key belonging to the parent product.
site_urlstringProtected productsAllFull current website URL or host.
installation_idstring (UUID)Protected productsAllPersistent canonical installation UUID.

Purpose Behaviors

  • install: Used during initial setup. The server resolves the current published release authorized for this license. current_version must be omitted; providing it returns 400 current_version_not_allowed.
  • reinstall: Used to repair or replace an installation. Like install, current_version must be omitted. The server selects an authorized release; it does not promise the exact bytes previously installed.
  • update: Used when upgrading existing software. The client must provide the actual running version in current_version. If omitted or empty, the server returns 400 current_version_required. The server evaluates whether a newer published release exists and is entitled under the license's update window.

Request Examples

Initial install

json
{
  "slug": "crm-connector",
  "purpose": "install",
  "license_key": "WNW-7712-8823-9934",
  "site_url": "https://client-site.example",
  "installation_id": "550e8400-e29b-41d4-a716-446655440000"
}

Reinstall

json
{
  "slug": "crm-connector",
  "purpose": "reinstall",
  "license_key": "WNW-7712-8823-9934",
  "site_url": "https://client-site.example",
  "installation_id": "550e8400-e29b-41d4-a716-446655440000"
}

Update

json
{
  "slug": "crm-connector",
  "purpose": "update",
  "current_version": "1.2.0",
  "license_key": "WNW-7712-8823-9934",
  "site_url": "https://client-site.example",
  "installation_id": "550e8400-e29b-41d4-a716-446655440000"
}

Successful Local Package Response

When an eligible local package is available, WooNooW mints a single-use bearer token and returns package metadata:

json
{
  "success": true,
  "purpose": "update",
  "product": {
    "id": 1042,
    "name": "CRM Connector",
    "slug": "crm-connector"
  },
  "version_id": 88,
  "version": "1.3.0",
  "artifact_download_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "file_name": "crm-connector_v_1.3.0.zip",
  "file_size": 1548291,
  "file_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",
  "signature": null,
  "signing_key_id": null,
  "signed_at": null,
  "release_date": "2026-09-08 10:00:00",
  "manual_download_only": false,
  "download_url": "https://store.example/wp-json/woonoow/v1/software/download?token=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "token_expires_at": "2026-09-08 10:05:00",
  "token_binding_version": 1,
  "entitlement": {
    "license_required": true,
    "license_active": true,
    "update_entitled": true,
    "support_active": true,
    "usage_expires_at": "2027-09-08 10:00:00",
    "updates_expires_at": "2027-09-08 10:00:00",
    "support_expires_at": "2027-09-08 10:00:00",
    "policy_mode": "explicit",
    "historical_downloads": true
  }
}

3. Entitlement Semantics & UX

The entitlement object reflects the evaluated business state of the license. Use these attributes to drive client user experience:

Entitlement FieldTypeDescription & UI Treatment
license_activebooleanEffective server-side usage state after revocation, expiry, and subscription checks. Package access is denied when false; how an already-installed client limits functionality remains the publisher's product policy.
update_entitledbooleanWhether the update window is currently active. If false while license_active is true, post-cutoff releases are unavailable; an older eligible release may still be selected only when historical_downloads permits it.
support_activeboolean or nullActive technical support entitlement. null indicates support rights are unspecified in the policy snapshot.
usage_expires_atstring or nullUTC timestamp when the usage right expires; null means no configured usage expiry.
updates_expires_atstring or nullUTC cutoff timestamp for receiving software updates. Releases published after this date are not entitled.
support_expires_atstring or nullUTC cutoff timestamp for technical support.
historical_downloadsbooleanIf true, an active license with expired updates may install, reinstall, or update to a matching release published on or before updates_expires_at.

For complete rules on policy inheritance and snapshot modes, see License Entitlements.


4. Download Execution & Token Lifecycle

Local packages are redeemed using the temporary URL provided in download_url:

http
GET /wp-json/woonoow/v1/software/download?token={bearer-token}

Download Rules & Protocol Constraints

  • Immediate retrieval: Download URLs contain single-use tokens with a short time-to-live (default 5 minutes, configured by store token_expiry). Request the package immediately before downloading.
  • Do not cache or log tokens: Never persist download_url or bearer tokens in database options, long-lived caches, error logs, or telemetry.
  • No extra query parameters: The download endpoint accepts only token. Any additional parameters (such as version, file, or cache busters) result in 400 download_override_rejected.
  • No HEAD requests: Sending a HEAD request returns 405 head_not_supported and Accept-Ranges: none. HEAD is non-consuming, but clients must initiate download directly via GET.
  • No Range headers or resuming: Range/resume requests return 416 range_not_supported. Clients must download the entire archive in a single continuous stream.
  • Atomic token claim: WooNooW atomically marks the token row as consumed (used_at) before streaming file bytes. A token cannot be redeemed twice.
  • Deliberate refresh on token failure: If a transfer fails after claim or the server returns token_expired, token_consumed, or token_invalidated, do not blindly retry the same download URL. The client must issue a single fresh POST /software/package request to obtain a new token.

5. Package Integrity Verification

Before unpacking, executing, or installing a downloaded archive, the client must strictly verify SHA-256 integrity:

  1. 1. Validate expected hash format

    Confirm that file_sha256 from the package metadata consists of exactly 64 lowercase hexadecimal characters (/^[0-9a-f]{64}$/). Reject the package immediately if the checksum is missing or malformed.

  2. 2. Hash complete downloaded bytes

    Stream or buffer the complete downloaded archive and calculate its cryptographic SHA-256 digest.

  3. 3. Constant-time comparison

    Compare the computed digest against the expected file_sha256 using a constant-time string comparison function to prevent timing leaks.

  4. 4. Inspect response metadata

    The HTTP response from /software/download includes the supporting header X-Package-Sha256. Verify that this header matches file_sha256. Note that header verification is secondary; full byte verification is mandatory.

  5. 5. Fail closed on mismatch

    If the computed hash does not match file_sha256, delete the downloaded archive immediately, abort installation, and preserve the running software version.


6. External & Manual Releases

Merchants can configure software versions backed by external HTTPS URLs. In this case, WooNooW protects the release metadata behind entitlement authorization without serving bytes through the local updater token pipeline.

When an external release is resolved:

  • manual_download_only is true.
  • download_url, token_expires_at, and token_binding_version are null.
  • external_link_url provides a gated route:
    text
    GET /wp-json/woonoow/v1/software/products/{product_id}/versions/{version_id}/external-link
    

Client Handling of External Releases

Automatic client updaters must not treat external releases as automatic packages:

  1. Inspect manual_download_only. If true, abort automatic download.
  2. Direct the user to an explicit manual-download action; do not place license-bearing URLs in logs, analytics, or referrers.
  3. For a protected product, call external_link_url with license_key, site_url, installation_id, and the intended purpose. The route returns JSON containing the provider download_url, file_sha256, and manual_download: true; core does not proxy the bytes or perform the final browser redirect. The manual downloader should verify the provider bytes against that SHA-256 value before installation.

7. WordPress Updater Compatibility (/software/check)

WooNooW exposes GET|POST /software/check primarily for compatibility with WordPress core update transients (site_transient_update_plugins, plugins_api).

http
POST /wp-json/woonoow/v1/software/check
Content-Type: application/json

{
  "slug": "my-plugin",
  "version": "1.2.0",
  "license_key": "XXXX-YYYY-ZZZZ-WWWW",
  "site_url": "https://client-site.example",
  "installation_id": "550e8400-e29b-41d4-a716-446655440000"
}

Key Differences from /software/package

  • Installed version required: The version parameter is mandatory and must represent the running version. /software/check cannot be used for initial installations or reinstalls.
  • available vs. eligible:
    • available: Indicates whether a newer, artifact-valid release exists on the server, regardless of license entitlements.
    • eligible (and legacy update_available): Indicates whether this specific license and identity is authorized to download the newer release.
  • Cache TTL mismatch: WordPress transients often cache update check results for 12 hours (cache_ttl). Because bearer tokens in download_url expire within 5 minutes, an updater must never reuse a cached download_url. Always request a fresh package on the actual install/upgrade execution hook.

Bundled WordPress updater template

WooNooW includes templates/updater/class-woonoow-updater.php as a compatibility reference for:

  • creating an installation UUID scoped by store API URL + software slug;
  • calling /software/check for WordPress plugin/theme metadata; and
  • wiring update and details transients.

8. Complete Integration Examples

JavaScript (Node.js ESM)

This reference client demonstrates persistent UUID storage, purpose-aware package retrieval, manual-release avoidance, streamed byte verification, and single-attempt fresh-token recovery. Namespace the identity by store API and software slug so independent integrations do not accidentally share one UUID.

javascript
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
import { once } from 'node:events';
import { createWriteStream } from 'node:fs';
import { readFile, unlink, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { finished } from 'node:stream/promises';
import { homedir } from 'node:os';

const API_BASE = 'https://store.example/wp-json/woonoow/v1';
const SHA256_REGEX = /^[0-9a-f]{64}$/;

/**
 * Persist or retrieve a canonical installation UUID.
 */
async function getOrCreateInstallationId(slug) {
  const scope = createHash('sha256').update(`${API_BASE}|${slug}`).digest('hex').slice(0, 16);
  const identityFile = join(homedir(), `.woonoow-${scope}-installation-id`);

  try {
    const existing = (await readFile(identityFile, 'utf8')).trim().toLowerCase();
    if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(existing)) {
      return existing;
    }
  } catch {
    // Identity file does not exist yet; generate below.
  }

  const newId = randomUUID();
  await writeFile(identityFile, newId, { mode: 0o600 });
  return newId;
}

/**
 * Request a purpose-aware package payload from WooNooW.
 */
async function requestPackage({ slug, purpose, currentVersion, licenseKey, siteUrl, installationId }) {
  const payload = {
    slug,
    purpose,
    license_key: licenseKey,
    site_url: siteUrl,
    installation_id: installationId,
  };

  // current_version is strictly required for 'update' and forbidden for 'install'/'reinstall'
  if (purpose === 'update') {
    if (!currentVersion) throw new Error('current_version_required');
    payload.current_version = currentVersion;
  }

  const response = await fetch(`${API_BASE}/software/package`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
    body: JSON.stringify(payload),
  });

  const data = await response.json().catch(() => ({}));
  if (!response.ok) {
    const err = new Error(data.message || `HTTP ${response.status}`);
    err.code = data.code ?? data.error ?? `http_${response.status}`;
    err.status = response.status;
    err.retryAfter = data.data?.retry_after;
    throw err;
  }

  if (!data || typeof data !== 'object' || !SHA256_REGEX.test(String(data.file_sha256 || '').toLowerCase())) {
    throw new Error('invalid_package_response');
  }
  if (data.manual_download_only ? !data.external_link_url : !data.download_url) {
    throw new Error('invalid_package_response');
  }

  return data;
}

/**
 * Download package bytes and verify SHA-256 integrity.
 */
async function downloadAndVerify(downloadUrl, expectedHash, destinationPath) {
  const expected = String(expectedHash || '').toLowerCase();
  if (!SHA256_REGEX.test(expected)) {
    throw new Error('invalid_expected_checksum');
  }

  let response;
  try {
    // Pure GET: no HEAD probe, Range header, or extra query selector.
    response = await fetch(downloadUrl, {
      method: 'GET',
      headers: { 'Accept': 'application/octet-stream' },
    });
  } catch (cause) {
    const err = new Error('package_transport_failed', { cause });
    err.retryWithFreshToken = true;
    throw err;
  }

  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    const err = new Error(errorData.message || `HTTP ${response.status}`);
    err.code = errorData.code ?? errorData.error ?? `http_${response.status}`;
    err.status = response.status;
    throw err;
  }

  const advertised = response.headers.get('X-Package-Sha256')?.trim().toLowerCase();
  if (advertised && advertised !== expected) {
    throw new Error('package_checksum_metadata_mismatch');
  }
  if (!response.body) {
    const err = new Error('package_response_body_missing');
    err.retryWithFreshToken = true;
    throw err;
  }

  const hasher = createHash('sha256');
  const writer = createWriteStream(destinationPath, { flags: 'w', mode: 0o600 });
  const reader = response.body.getReader();

  try {
    while (true) {
      let result;
      try {
        result = await reader.read();
      } catch (cause) {
        const err = new Error('package_transport_interrupted', { cause });
        err.retryWithFreshToken = true;
        throw err;
      }

      if (result.done) break;
      hasher.update(result.value);
      if (!writer.write(result.value)) {
        await once(writer, 'drain');
      }
    }

    writer.end();
    await finished(writer);
  } catch (err) {
    writer.destroy();
    await unlink(destinationPath).catch(() => {});
    throw err;
  }

  const actual = hasher.digest('hex').toLowerCase();
  const expectedBuffer = Buffer.from(expected, 'utf8');
  const actualBuffer = Buffer.from(actual, 'utf8');
  if (expectedBuffer.length !== actualBuffer.length || !timingSafeEqual(expectedBuffer, actualBuffer)) {
    await unlink(destinationPath).catch(() => {});
    throw new Error('package_integrity_failed');
  }

  return destinationPath;
}

/**
 * Main workflow: execute an update or install with one deliberate fresh-token retry.
 */
export async function installOrUpdateSoftware({ slug, purpose, currentVersion, licenseKey, siteUrl, destinationPath }) {
  const installationId = await getOrCreateInstallationId(slug);
  const retryableTokenCodes = new Set(['token_expired', 'token_consumed', 'token_invalidated']);

  for (let attempt = 0; attempt < 2; attempt += 1) {
    const packageInfo = await requestPackage({
      slug,
      purpose,
      currentVersion,
      licenseKey,
      siteUrl,
      installationId,
    });

    if (packageInfo.manual_download_only) {
      const err = new Error('manual_download_required');
      err.externalLinkUrl = packageInfo.external_link_url;
      throw err;
    }

    try {
      return await downloadAndVerify(
        packageInfo.download_url,
        packageInfo.file_sha256,
        destinationPath,
      );
    } catch (err) {
      const mayRefresh = retryableTokenCodes.has(err.code) || err.retryWithFreshToken === true;
      if (attempt === 0 && mayRefresh) {
        continue;
      }
      throw err;
    }
  }

  throw new Error('package_download_failed');
}

Python 3 Integration

python
import hashlib
import hmac
import os
import re
import tempfile
import uuid
from pathlib import Path

import requests

API_BASE = "https://store.example/wp-json/woonoow/v1"
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
RETRYABLE_TOKEN_CODES = {"token_expired", "token_consumed", "token_invalidated"}


class WooNooWError(RuntimeError):
    def __init__(self, code: str, message: str, *, retry_with_fresh_token: bool = False):
        super().__init__(message)
        self.code = code
        self.retry_with_fresh_token = retry_with_fresh_token


def get_or_create_installation_id(slug: str) -> str:
    """Retrieve a UUID scoped by WooNooW API base and software slug."""
    scope = hashlib.sha256(f"{API_BASE}|{slug}".encode()).hexdigest()[:16]
    identity_file = Path.home() / f".woonoow-{scope}-installation-id"

    if identity_file.exists():
        saved = identity_file.read_text(encoding="utf-8").strip().lower()
        try:
            return str(uuid.UUID(saved))
        except ValueError:
            pass

    new_id = str(uuid.uuid4())
    identity_file.write_text(new_id, encoding="utf-8")
    identity_file.chmod(0o600)
    return new_id


def response_error(response: requests.Response) -> WooNooWError:
    try:
        body = response.json()
    except ValueError:
        body = {}

    code = body.get("code") or body.get("error") or f"http_{response.status_code}"
    message = body.get("message") or f"HTTP {response.status_code}"
    return WooNooWError(code, message)


def request_package(
    slug: str,
    purpose: str,
    license_key: str,
    site_url: str,
    current_version: str | None = None,
) -> dict:
    """Fetch protected-product metadata via POST /software/package."""
    payload = {
        "slug": slug,
        "purpose": purpose,
        "license_key": license_key,
        "site_url": site_url,
        "installation_id": get_or_create_installation_id(slug),
    }

    if purpose == "update":
        if not current_version:
            raise ValueError("current_version is required for update purpose")
        payload["current_version"] = current_version

    response = requests.post(
        f"{API_BASE}/software/package",
        json=payload,
        headers={"Accept": "application/json"},
        timeout=20,
    )
    if not response.ok:
        raise response_error(response)

    try:
        body = response.json()
    except ValueError as cause:
        raise WooNooWError("invalid_package_response", "Package response was not JSON.") from cause

    checksum = str(body.get("file_sha256", "")).lower()
    expected_link = body.get("external_link_url") if body.get("manual_download_only") else body.get("download_url")
    if not SHA256_PATTERN.fullmatch(checksum) or not expected_link:
        raise WooNooWError("invalid_package_response", "Package response metadata is incomplete.")
    return body


def download_package(download_url: str, expected_hash: str, output_path: Path) -> Path:
    """Perform one full GET and verify SHA-256 before returning the file."""
    expected = expected_hash.strip().lower()
    if not SHA256_PATTERN.fullmatch(expected):
        raise ValueError("Invalid expected SHA-256 checksum")

    digest = hashlib.sha256()
    try:
        try:
            response = requests.get(download_url, stream=True, timeout=60)
        except requests.RequestException as cause:
            raise WooNooWError(
                "package_transport_failed",
                str(cause),
                retry_with_fresh_token=True,
            ) from cause

        with response:
            if not response.ok:
                raise response_error(response)

            advertised = response.headers.get("X-Package-Sha256", "").strip().lower()
            if advertised and not hmac.compare_digest(advertised, expected):
                raise WooNooWError(
                    "package_checksum_metadata_mismatch",
                    "The download header does not match package metadata.",
                )

            try:
                with output_path.open("wb") as package:
                    for chunk in response.iter_content(chunk_size=1024 * 1024):
                        if chunk:
                            digest.update(chunk)
                            package.write(chunk)
            except requests.RequestException as cause:
                raise WooNooWError(
                    "package_transport_interrupted",
                    str(cause),
                    retry_with_fresh_token=True,
                ) from cause
    except Exception:
        output_path.unlink(missing_ok=True)
        raise

    actual = digest.hexdigest().lower()
    if not hmac.compare_digest(actual, expected):
        output_path.unlink(missing_ok=True)
        raise WooNooWError("package_integrity_failed", "Downloaded bytes failed SHA-256 verification.")

    return output_path


def get_software_package(
    slug: str,
    purpose: str,
    license_key: str,
    site_url: str,
    current_version: str | None = None,
) -> Path:
    """Download with at most one deliberate fresh-token retry."""
    for attempt in range(2):
        info = request_package(slug, purpose, license_key, site_url, current_version)
        if info.get("manual_download_only"):
            raise WooNooWError(
                "manual_download_required",
                f"Use the gated route: {info.get('external_link_url')}",
            )

        descriptor, name = tempfile.mkstemp(prefix="woonoow-", suffix=".zip")
        os.close(descriptor)
        destination = Path(name)

        try:
            return download_package(info["download_url"], info["file_sha256"], destination)
        except WooNooWError as error:
            may_refresh = error.code in RETRYABLE_TOKEN_CODES or error.retry_with_fresh_token
            if attempt == 0 and may_refresh:
                continue
            raise

    raise WooNooWError("package_download_failed", "Package download failed.")

9. Error Handling Reference

Normalize the machine-readable code as body.code ?? body.error, because current software routes include both standard WordPress REST errors and compatibility response envelopes. Branch on that code and HTTP status rather than matching translated messages.

Software preflight errors

Preflight errors occur before candidate release resolution begins. They indicate invalid input, disabled features, or failure of requesting identity and base license validation.

HTTP StatusError CodeMeaning & Client Action
400missing_paramsRequired parameters (slug, purpose) are missing.
400unsupported_parameterExtra parameters were submitted. Remove client overrides.
400invalid_purposepurpose is not install, reinstall, or update.
400current_version_requiredpurpose=update was called without a valid current_version.
400current_version_not_allowedcurrent_version was passed during install or reinstall. Remove it.
400missing_identitysite_url or installation_id was omitted for a protected product.
400invalid_identityinstallation_id is not a valid UUID or domain could not be parsed.
403software_disabledSoftware distribution is disabled for this product.
403license_requiredProduct requires licensing, but no license key was provided.
403invalid_licenseLicense key does not exist or has been deleted. Prompt user for license key.
403license_product_mismatchLicense belongs to a different WooCommerce product. Fail closed.
403license_usage_inactiveLicense is revoked, expired, or attached native subscription is lapsed. Block access.
403domain_not_activatedThe UUID + domain identity has no active activation. Perform activation first.
404product_not_foundNo published WooCommerce product matches the requested slug.
409software_slug_ambiguousMultiple published WooCommerce products share the same slug. Store configuration error.
429rate_limitedIssuance rate limit reached. Inspect data.retry_after (seconds) in the JSON response and back off.
503module_disabledSoftware Distribution module is disabled on the store.
503licensing_unavailableProtected product requested while Licensing module is disabled.

Candidate selection & release authorization errors

Evaluated while selecting and authorizing published candidate releases.

HTTP StatusError CodeMeaning & Client Action
403no_eligible_releasePublished candidate releases exist for the product, but none satisfy release authorization or artifact validation (e.g., all candidates fall outside the update window without historical access).
404no_eligible_releaseNo published candidate releases match the request at all (e.g. no published releases exist, or for update no release is newer than current_version).
403release_not_entitledThe candidate release falls outside the license update cutoff and historical-access policy. Show the applicable renewal/access message.
403release_product_not_entitledThe release does not belong to the parent product authorized by the license.
404artifact_not_foundArtifact file is missing from the server vault.
410release_withdrawnRelease was withdrawn by the store administrator.
500artifact_integrity_failedPhysical archive hash does not match the database checksum.

Download execution & token redemption errors

Evaluated during token redemption at GET /software/download?token=....

HTTP StatusError CodeMeaning & Client Action
400missing_tokenThe token query parameter was empty or omitted.
400download_override_rejectedAdditional query parameters were passed to /software/download. Only token is accepted.
403invalid_tokenBearer token hash was not found in the database.
403token_binding_requiredToken lacks the current binding schema. Request a fresh package.
403token_expiredThe short-lived bearer token expired (60s TTL). Request a fresh package via POST /software/package.
403token_consumedToken was already claimed or lost an atomic claim race. Request a fresh package.
403token_invalidatedRelease was withdrawn or license state changed. Request a fresh package.
403activation_inactiveInstallation activation was deactivated prior to download redemption.
403activation_binding_changedActivation revision or identity changed since token issuance. Request a new package.
405head_not_supportedHEAD request sent to /software/download. Use GET. Non-consuming.
416range_not_supportedRange header sent to /software/download. Download full file without chunking.

Last updated Sep 8, 2026