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.
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.
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:
Parameter
Type
Required
Purpose Rules
Description
slug
string
Yes
All
Unique software slug configured on the parent WooCommerce product.
purpose
string
Yes
All
Must be exactly install, reinstall, or update.
current_version
string
Conditional
Required for update. Must be omitted for install and reinstall.
The actual currently installed software version.
license_key
string
Protected products
All
License key belonging to the parent product.
site_url
string
Protected products
All
Full current website URL or host.
installation_id
string (UUID)
Protected products
All
Persistent canonical installation UUID.
Purpose Behaviors
install: Used during initial setup. The server resolves the current published release authorized for this license. current_versionmust be omitted; providing it returns 400 current_version_not_allowed.
reinstall: Used to repair or replace an installation. Like install, current_versionmust 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.
The entitlement object reflects the evaluated business state of the license. Use these attributes to drive client user experience:
Entitlement Field
Type
Description & UI Treatment
license_active
boolean
Effective 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_entitled
boolean
Whether 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_active
boolean or null
Active technical support entitlement. null indicates support rights are unspecified in the policy snapshot.
usage_expires_at
string or null
UTC timestamp when the usage right expires; null means no configured usage expiry.
updates_expires_at
string or null
UTC cutoff timestamp for receiving software updates. Releases published after this date are not entitled.
support_expires_at
string or null
UTC cutoff timestamp for technical support.
historical_downloads
boolean
If 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:
Inspect manual_download_only. If true, abort automatic download.
Direct the user to an explicit manual-download action; do not place license-bearing URLs in logs, analytics, or referrers.
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.
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';constAPI_BASE='https://store.example/wp-json/woonoow/v1';constSHA256_REGEX=/^[0-9a-f]{64}$/;/**
* Persist or retrieve a canonical installation UUID.
*/asyncfunctiongetOrCreateInstallationId(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 =(awaitreadFile(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();awaitwriteFile(identityFile, newId,{mode:0o600});return newId;}/**
* Request a purpose-aware package payload from WooNooW.
*/asyncfunctionrequestPackage({ 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)thrownewError('current_version_required'); payload.current_version= currentVersion;}const response =awaitfetch(`${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 =newError(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())){thrownewError('invalid_package_response');}if(data.manual_download_only?!data.external_link_url:!data.download_url){thrownewError('invalid_package_response');}return data;}/**
* Download package bytes and verify SHA-256 integrity.
*/asyncfunctiondownloadAndVerify(downloadUrl, expectedHash, destinationPath){const expected =String(expectedHash ||'').toLowerCase();if(!SHA256_REGEX.test(expected)){thrownewError('invalid_expected_checksum');}let response;try{// Pure GET: no HEAD probe, Range header, or extra query selector. response =awaitfetch(downloadUrl,{method:'GET',headers:{'Accept':'application/octet-stream'},});}catch(cause){const err =newError('package_transport_failed',{ cause }); err.retryWithFreshToken=true;throw err;}if(!response.ok){const errorData =await response.json().catch(()=>({}));const err =newError(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){thrownewError('package_checksum_metadata_mismatch');}if(!response.body){const err =newError('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 =newError('package_transport_interrupted',{ cause }); err.retryWithFreshToken=true;throw err;}if(result.done)break; hasher.update(result.value);if(!writer.write(result.value)){awaitonce(writer,'drain');}} writer.end();awaitfinished(writer);}catch(err){ writer.destroy();awaitunlink(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)){awaitunlink(destinationPath).catch(()=>{});thrownewError('package_integrity_failed');}return destinationPath;}/**
* Main workflow: execute an update or install with one deliberate fresh-token retry.
*/exportasyncfunctioninstallOrUpdateSoftware({ slug, purpose, currentVersion, licenseKey, siteUrl, destinationPath }){const installationId =awaitgetOrCreateInstallationId(slug);const retryableTokenCodes =newSet(['token_expired','token_consumed','token_invalidated']);for(let attempt =0; attempt <2; attempt +=1){const packageInfo =awaitrequestPackage({ slug, purpose, currentVersion, licenseKey, siteUrl, installationId,});if(packageInfo.manual_download_only){const err =newError('manual_download_required'); err.externalLinkUrl= packageInfo.external_link_url;throw err;}try{returnawaitdownloadAndVerify( 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;}}thrownewError('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"}classWooNooWError(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
defget_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:returnstr(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
defresponse_error(response: requests.Response)-> WooNooWError:try: body = response.json()except ValueError: body ={} code = body.get("code")or body.get("error")orf"http_{response.status_code}" message = body.get("message")orf"HTTP {response.status_code}"return WooNooWError(code, message)defrequest_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":ifnot 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,)ifnot 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")ifnot SHA256_PATTERN.fullmatch(checksum)ornot expected_link:raise WooNooWError("invalid_package_response","Package response metadata is incomplete.")return body
defdownload_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()ifnot 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:ifnot response.ok:raise response_error(response) advertised = response.headers.get("X-Package-Sha256","").strip().lower()if advertised andnot 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()ifnot 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
defget_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 inrange(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 ==0and may_refresh:continueraiseraise 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 Status
Error Code
Meaning & Client Action
400
missing_params
Required parameters (slug, purpose) are missing.
400
unsupported_parameter
Extra parameters were submitted. Remove client overrides.
400
invalid_purpose
purpose is not install, reinstall, or update.
400
current_version_required
purpose=update was called without a valid current_version.
400
current_version_not_allowed
current_version was passed during install or reinstall. Remove it.
400
missing_identity
site_url or installation_id was omitted for a protected product.
400
invalid_identity
installation_id is not a valid UUID or domain could not be parsed.
403
software_disabled
Software distribution is disabled for this product.
403
license_required
Product requires licensing, but no license key was provided.
403
invalid_license
License key does not exist or has been deleted. Prompt user for license key.
403
license_product_mismatch
License belongs to a different WooCommerce product. Fail closed.
403
license_usage_inactive
License is revoked, expired, or attached native subscription is lapsed. Block access.
403
domain_not_activated
The UUID + domain identity has no active activation. Perform activation first.
404
product_not_found
No published WooCommerce product matches the requested slug.
409
software_slug_ambiguous
Multiple published WooCommerce products share the same slug. Store configuration error.
429
rate_limited
Issuance rate limit reached. Inspect data.retry_after (seconds) in the JSON response and back off.
503
module_disabled
Software Distribution module is disabled on the store.
503
licensing_unavailable
Protected product requested while Licensing module is disabled.
Evaluated while selecting and authorizing published candidate releases.
HTTP Status
Error Code
Meaning & Client Action
403
no_eligible_release
Published 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).
404
no_eligible_release
No published candidate releases match the request at all (e.g. no published releases exist, or for update no release is newer than current_version).
403
release_not_entitled
The candidate release falls outside the license update cutoff and historical-access policy. Show the applicable renewal/access message.
403
release_product_not_entitled
The release does not belong to the parent product authorized by the license.
404
artifact_not_found
Artifact file is missing from the server vault.
410
release_withdrawn
Release was withdrawn by the store administrator.
500
artifact_integrity_failed
Physical archive hash does not match the database checksum.
Download execution & token redemption errors
Evaluated during token redemption at GET /software/download?token=....
HTTP Status
Error Code
Meaning & Client Action
400
missing_token
The token query parameter was empty or omitted.
400
download_override_rejected
Additional query parameters were passed to /software/download. Only token is accepted.
403
invalid_token
Bearer token hash was not found in the database.
403
token_binding_required
Token lacks the current binding schema. Request a fresh package.
403
token_expired
The short-lived bearer token expired (60s TTL). Request a fresh package via POST /software/package.
403
token_consumed
Token was already claimed or lost an atomic claim race. Request a fresh package.
403
token_invalidated
Release was withdrawn or license state changed. Request a fresh package.
403
activation_inactive
Installation activation was deactivated prior to download redemption.
403
activation_binding_changed
Activation revision or identity changed since token issuance. Request a new package.
405
head_not_supported
HEAD request sent to /software/download. Use GET. Non-consuming.
416
range_not_supported
Range header sent to /software/download. Download full file without chunking.