Software Updates API

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

  1. Enable Licensing under Settings → Modules.
  2. Enable Software Distribution under Settings → Modules.
  3. Create a WooCommerce product with Downloadable enabled.
  4. Enable licensing and software distribution for that product.
  5. Assign a unique software slug, such as my-plugin.
  6. 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:

FieldDescription
Enable Software DistributionAllows the product to participate in update checks and secure downloads.
Software SlugStable unique identifier sent by clients in update checks.
WordPress ProductEnables WordPress compatibility metadata.
Requires WPMinimum supported WordPress version.
Tested up to WPLatest tested WordPress version.
Requires PHPMinimum 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:

text
artifact_download_id
file_name
file_size
file_sha256

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/json
X-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
json
{
  "license_key": "XXXX-YYYY-ZZZZ-WWWW",
  "slug": "my-plugin",
  "version": "1.0.0",
  "site_url": "https://customer-site.com",
  "installation_id": "550e8400-e29b-41d4-a716-446655440000"
}
ParameterTypeRequiredDescription
license_keystringYesLicense key issued for this product.
slugstringYesExact software slug configured on the product.
versionstringYesCurrently installed version.
site_urlURL or hostYesCurrent website URL; normalized by the server.
installation_idUUIDYesPersistent canonical installation UUID.

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:

json
{
  "wordpress": {
    "requires": "6.0",
    "tested": "6.7",
    "requires_php": "7.4"
  }
}

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:

  1. perform a fresh software check on the download/install path;
  2. use the returned URL promptly;
  3. perform another software check when the server returns token_expired or token_consumed;
  4. 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:

  1. validate that file_sha256 is 64 hexadecimal characters;
  2. download the package to memory or a temporary file;
  3. calculate SHA-256 over the completed package;
  4. compare it with file_sha256 using a constant-time comparison where available;
  5. optionally confirm that X-Package-Sha256 matches the update metadata;
  6. 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;
  • WordPress plugin/theme update metadata integration.

Example initialization:

php
require_once plugin_dir_path(__FILE__) . 'includes/class-woonoow-updater.php';

new WooNooW_Updater([
    'api_url'     => 'https://your-store.com/',
    'slug'        => 'my-plugin',
    'version'     => MY_PLUGIN_VERSION,
    'license_key' => get_option('my_plugin_license_key'),
    'plugin_file' => __FILE__,
]);

JavaScript example

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';

const API_BASE = 'https://your-store.com/wp-json/woonoow/v1';
const IDENTITY_FILE = join(homedir(), '.my-software-installation-id');
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;

async function getInstallationId() {
  try {
    const saved = (await readFile(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();
  await writeFile(IDENTITY_FILE, installationId, { mode: 0o600 });
  return installationId;
}

async function downloadVerifiedUpdate(licenseKey, currentVersion, siteUrl) {
  const checkResponse = await fetch(`${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: await getInstallationId(),
    }),
  });

  const update = await checkResponse.json();

  if (!checkResponse.ok) {
    throw new Error(update.code || update.error || 'update_check_failed');
  }

  if (!update.update_available) {
    return null;
  }

  if (!/^[a-f0-9]{64}$/i.test(update.file_sha256 || '')) {
    throw new Error('missing_or_invalid_package_checksum');
  }

  const packageResponse = await fetch(update.download_url);

  if (!packageResponse.ok) {
    throw new Error('package_download_failed');
  }

  const expected = update.file_sha256.toLowerCase();
  const advertisedHeader = packageResponse.headers.get('X-Package-Sha256');

  if (advertisedHeader && advertisedHeader.toLowerCase() !== expected) {
    throw new Error('package_checksum_metadata_mismatch');
  }

  const bytes = Buffer.from(await packageResponse.arrayBuffer());
  const actual = createHash('sha256').update(bytes).digest('hex');

  if (actual !== expected) {
    throw new Error('package_integrity_failed');
  }

  return bytes;
}

Do not execute or install the returned bytes until the checksum comparison succeeds.

Python example

python
import hashlib
import hmac

import re
import tempfile
import uuid
from pathlib import Path

import requests

API_BASE = 'https://your-store.com/wp-json/woonoow/v1'
IDENTITY_FILE = Path.home() / '.my-software-installation-id'


def get_installation_id() -> str:
    if IDENTITY_FILE.exists():
        value = IDENTITY_FILE.read_text(encoding='utf-8').strip().lower()
        try:
            return str(uuid.UUID(value))
        except ValueError:
            pass

    value = str(uuid.uuid4())
    IDENTITY_FILE.write_text(value, encoding='utf-8')
    return value


def download_verified_update(
    license_key: str,
    current_version: str,
    site_url: str,
) -> Path | None:
    check = requests.post(
        f'{API_BASE}/software/check',
        json={
            'license_key': license_key,
            'slug': 'my-software',
            'version': current_version,
            'site_url': site_url,
            'installation_id': get_installation_id(),
        },
        timeout=30,
    )
    check.raise_for_status()
    update = check.json()

    if not update.get('update_available'):
        return None

    expected = str(update.get('file_sha256', '')).lower()
    if not re.fullmatch(r'[a-f0-9]{64}', expected):
        raise RuntimeError('Missing or invalid package checksum')

    digest = hashlib.sha256()
    with requests.get(update['download_url'], stream=True, timeout=60) as response:
        response.raise_for_status()

        header_hash = response.headers.get('X-Package-Sha256', '').lower()
        if header_hash and not hmac.compare_digest(header_hash, expected):
            raise RuntimeError('Package checksum metadata mismatch')

        with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as package:
            package_path = Path(package.name)
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                if chunk:
                    digest.update(chunk)
                    package.write(chunk)

    if not hmac.compare_digest(digest.hexdigest(), expected):
        package_path.unlink(missing_ok=True)
        raise RuntimeError('Package integrity verification failed')

    return package_path

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.

HTTPCodeMeaning and client action
400missing_paramsOne of the basic update parameters is missing. Fix the request.
400missing_identitysite_url or installation_id is missing. Fix the client.
400invalid_identityThe UUID or website URL is malformed. Fix the persisted identity/input.
403invalid_licenseThe license key is invalid. Ask the customer to verify it.
403domain_not_activatedThe UUID + domain identity has no active activation. Activate it first.
403license_expired / expiredThe license has expired.
403subscription_inactiveA linked subscription is not active.
403revoked / license_inactiveThe license was revoked or is inactive.
403product_not_licensedThe license is not entitled to the product resolved by slug.
403version_not_entitledThe token/version is not bound to the entitled product. Refresh once, then report a release configuration problem.
403invalid_tokenThe download token is unknown or malformed. Perform a new software check; do not reuse the URL.
403token_expiredThe five-minute token expired. Perform a new software check.
403token_consumedThe token was already used. Perform a new software check.
404product_not_foundNo product matches the software slug.
404artifact_not_found / artifact_missingRelease artifact metadata or the package file is unavailable. Do not install.
500artifact_integrity_failedThe server's actual package hash differs from the release metadata. Do not bypass the failure.
503module_disabledSoftware 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.

Last updated Jul 29, 2026