Distribute licensed software updates with exact artifact binding and SHA-256 verification
Overview
The Software Distribution module lets a WooNooW store publish versioned packages for WordPress plugins, themes, and other software installed on a website with a stable URL.
The default delivery model uses:
license and product entitlement checks;
a persistent installation UUID plus the current normalized domain;
an exact version-to-downloadable-file binding;
short-lived, single-use download tokens;
automatic SHA-256 metadata and server-side verification;
client-side SHA-256 verification before installation.
Asymmetric signing is optional and is not required for the default update flow.
Prerequisites
Enable Licensing under Settings → Modules.
Enable Software Distribution under Settings → Modules.
Create a WooCommerce product with Downloadable enabled.
Enable licensing and software distribution for that product.
Assign a unique software slug, such as my-plugin.
Add each release package as a WooCommerce downloadable file.
Software Distribution depends on Licensing. A valid license for one product does not authorize a package belonging to another product.
Configure a software product
The product's Software Distribution settings include:
Field
Description
Enable Software Distribution
Allows the product to participate in update checks and secure downloads.
Software Slug
Stable unique identifier sent by clients in update checks.
WordPress Product
Enables WordPress compatibility metadata.
Requires WP
Minimum supported WordPress version.
Tested up to WP
Latest tested WordPress version.
Requires PHP
Minimum supported PHP version.
Do not reuse a software slug across products. The slug is used to resolve the product whose entitlement and release metadata must be validated.
Publish a version
Every version must be bound to one exact entry from the product's WooCommerce downloadable files. The release operation does not select the first file automatically.
The required release metadata is:
text
version + product_id + artifact_download_id
artifact_download_id is the ID of the selected WooCommerce downloadable-file entry. When a version is created or updated, WooNooW resolves that file and automatically records:
If the artifact ID is missing, does not belong to the product, cannot be resolved to a local file, or cannot be hashed, the release is rejected.
Admin release API
The authenticated admin endpoint requires a user with the manage_woocommerce capability:
http
POST /wp-json/woonoow/v1/software/products/{product_id}/versions
Content-Type:application/jsonX-WP-Nonce:<rest-nonce>
json
{"version":"1.2.0","set_current":true,"artifact_download_id":"woocommerce-download-id","changelog":{"narrative":"Maintenance and compatibility release.","points":[{"type":"ADD","text":"Added feature X."},{"type":"FIX","text":"Fixed issue Y."}]}}
The same artifact_download_id requirement applies when editing a version.
Required website identity
Update checks identify the requesting website using:
text
persistent installation UUID + normalized domain
Both values are required. There is no domain-only, UUID-only, or machine_id fallback. See Website Identity for generation, persistence, normalization, and migration rules.
Check for updates
http
GET /wp-json/woonoow/v1/software/check
POST /wp-json/woonoow/v1/software/check
Use JSON POST requests for new integrations:
http
POST /wp-json/woonoow/v1/software/check
Content-Type:application/json
Before returning an update, WooNooW verifies the effective license lifecycle, active combined identity, product entitlement, requested version relationship, and release artifact metadata.
Update-available response
json
{"success":true,"update_available":true,"product":{"name":"My Plugin","slug":"my-plugin"},"current_version":"1.0.0","latest_version":"1.2.0","changelog":{"narrative":"Maintenance and compatibility release.","points":[{"type":"FIX","text":"Fixed issue Y."}]},"release_date":"2026-07-29 12:00:00","file_size":123456,"file_sha256":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","signature":null,"signing_key_id":null,"download_url":"https://your-store.com/wp-json/woonoow/v1/software/download?token=...","changelog_url":"https://your-store.com/wp-json/woonoow/v1/software/changelog?slug=my-plugin"}
For WordPress products, a wordpress object is also included:
file_sha256 is required when an update is available. signature and signing_key_id are nullable metadata reserved for optional advanced signing.
Download a package
http
GET /wp-json/woonoow/v1/software/download?token=<token>
A download token:
has a five-minute UTC lifetime;
is stored by WooNooW only as a SHA-256 hash;
is bound to the license, product, and version;
can be claimed only once;
is claimed atomically;
is consumed only after lifecycle, entitlement, artifact existence, and actual artifact checksum checks pass.
A successful binary response includes:
http
X-Package-Sha256:<64-character-sha256>
The server recalculates the artifact hash immediately before serving it. A missing or changed artifact is rejected without consuming the token.
Do not cache a download URL as package metadata
The tokenized download_url is a temporary credential, not a permanent package URL. WordPress update metadata and application caches may live much longer than the token's five-minute lifetime.
Your client must:
perform a fresh software check on the download/install path;
use the returned URL promptly;
perform another software check when the server returns token_expired or token_consumed;
avoid logging or persistently storing the raw token.
Verify SHA-256 before installation
The client must verify the downloaded bytes before unzip, execution, or installation:
validate that file_sha256 is 64 hexadecimal characters;
download the package to memory or a temporary file;
calculate SHA-256 over the completed package;
compare it with file_sha256 using a constant-time comparison where available;
optionally confirm that X-Package-Sha256 matches the update metadata;
delete/reject the package and stop installation on any mismatch.
HTTPS and server-side verification do not replace this final client-side check.
WordPress client integration
WooNooW includes a reference updater at:
text
templates/updater/class-woonoow-updater.php
It can be used as a reference for:
persistent installation UUID creation;
license validation and update-check request shapes;
This server-side JavaScript example stores one installation UUID in a durable file, performs a fresh update check, and verifies the downloaded bytes. Use your application's protected settings store in production.
javascript
import{ createHash, randomUUID }from'node:crypto';import{ readFile, writeFile }from'node:fs/promises';import{ homedir }from'node:os';import{ join }from'node:path';constAPI_BASE='https://your-store.com/wp-json/woonoow/v1';constIDENTITY_FILE=join(homedir(),'.my-software-installation-id');constUUID_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;asyncfunctiongetInstallationId(){try{const saved =(awaitreadFile(IDENTITY_FILE,'utf8')).trim().toLowerCase();if(UUID_PATTERN.test(saved)){return saved;}}catch{// Create the identity below when it has not been stored yet.}const installationId =randomUUID();awaitwriteFile(IDENTITY_FILE, installationId,{mode:0o600});return installationId;}asyncfunctiondownloadVerifiedUpdate(licenseKey, currentVersion, siteUrl){const checkResponse =awaitfetch(`${API_BASE}/software/check`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({license_key: licenseKey,slug:'my-software',version: currentVersion,site_url: siteUrl,installation_id:awaitgetInstallationId(),}),});const update =await checkResponse.json();if(!checkResponse.ok){thrownewError(update.code|| update.error||'update_check_failed');}if(!update.update_available){returnnull;}if(!/^[a-f0-9]{64}$/i.test(update.file_sha256||'')){thrownewError('missing_or_invalid_package_checksum');}const packageResponse =awaitfetch(update.download_url);if(!packageResponse.ok){thrownewError('package_download_failed');}const expected = update.file_sha256.toLowerCase();const advertisedHeader = packageResponse.headers.get('X-Package-Sha256');if(advertisedHeader && advertisedHeader.toLowerCase()!== expected){thrownewError('package_checksum_metadata_mismatch');}const bytes =Buffer.from(await packageResponse.arrayBuffer());const actual =createHash('sha256').update(bytes).digest('hex');if(actual !== expected){thrownewError('package_integrity_failed');}return bytes;}
Do not execute or install the returned bytes until the checksum comparison succeeds.
Delete the temporary file after installation or whenever the update is abandoned.
Get changelogs
http
GET /wp-json/woonoow/v1/software/changelog?slug=<slug>
GET /wp-json/woonoow/v1/software/changelog?slug=<slug>&version=<version>
The response includes release metadata such as version, release_date, changelog, file_size, file_sha256, and nullable signing metadata. The all-versions response also includes download_count.
Error codes
Handle machine-readable codes rather than matching translated messages.
HTTP
Code
Meaning and client action
400
missing_params
One of the basic update parameters is missing. Fix the request.
400
missing_identity
site_url or installation_id is missing. Fix the client.
400
invalid_identity
The UUID or website URL is malformed. Fix the persisted identity/input.
403
invalid_license
The license key is invalid. Ask the customer to verify it.
403
domain_not_activated
The UUID + domain identity has no active activation. Activate it first.
403
license_expired / expired
The license has expired.
403
subscription_inactive
A linked subscription is not active.
403
revoked / license_inactive
The license was revoked or is inactive.
403
product_not_licensed
The license is not entitled to the product resolved by slug.
403
version_not_entitled
The token/version is not bound to the entitled product. Refresh once, then report a release configuration problem.
403
invalid_token
The download token is unknown or malformed. Perform a new software check; do not reuse the URL.
403
token_expired
The five-minute token expired. Perform a new software check.
403
token_consumed
The token was already used. Perform a new software check.
404
product_not_found
No product matches the software slug.
404
artifact_not_found / artifact_missing
Release artifact metadata or the package file is unavailable. Do not install.
500
artifact_integrity_failed
The server's actual package hash differs from the release metadata. Do not bypass the failure.
503
module_disabled
Software Distribution is disabled on the store.
Optional advanced signing
The default mode does not require a signing key, CI provider, GitHub, Gitea, or cryptographic setup from the WooNooW store owner.
The current release endpoints do not accept package signatures. If asymmetric package signing is implemented later as a separate end-to-end capability:
the publisher owns and protects the private key outside the WooNooW runtime;
the client needs a trusted public key and a signature verifier;
signature and signing_key_id identify the signed artifact;
signing policy, key distribution, rotation, and failure behavior must be implemented as a separate end-to-end contract.
A client using the default mode must not reject an otherwise valid update merely because signature is null.