Software Distribution REST API

Normative reference for software package issuance, update checking, one-time token redemption, and external manual releases

Overview

The Software Distribution REST API governs the issuance, authorization, verification, and redemption of versioned software packages in WooNooW. It provides endpoints for automated updaters, bootstrap installers, reinstall operations, and manual customer downloads. A protected package request is not the activation bootstrap itself: the exact website identity must already have an active license activation.

Base URL: https://your-store.com/wp-json/woonoow/v1

Package resolution enforces server-side release selection and SHA-256 artifact integrity. Eligible local releases use short-lived, single-use bearer tokens; eligible external releases use a separate gated manual-link flow and never mint updater tokens. Clients never request or specify target releases, destination filenames, filesystem paths, download URLs, storage drivers, or checksum overrides.


Mental model and architecture

Software distribution in WooNooW strictly separates the product catalog hierarchy from release delivery streams.

graph TD
    A[Parent WooCommerce Product & Software Slug] --> B{Licensing Policy}
    B -->|Protected| C[License Key & Active Website Identity]
    B -->|Explicitly Public| D[Unlicensed Public Access]
    C --> E[Parent Release Stream]
    D --> E
    E --> F[Server Evaluates Published Releases]
    F --> G[Exact Immutable Artifact]
    G -->|Local Vault Package| H[Server-Selected Authorized Package & Single-Use Token]
    G -->|External Manual HTTPS| I[Manual-Only Flag & Gated External Link]

Key architectural invariants

  1. Parent product release stream: Releases belong strictly to the parent WooCommerce product identified by its unique slug. A product variation represents entitlement and pricing metadata (or a source download artifact), never an independent release stream.
  2. Fail-closed product classification: Product resolution searches only published WooCommerce product posts.
    • If zero products match the slug, resolution fails with 404 product_not_found.
    • If duplicate published products share a slug, resolution fails closed with 409 software_slug_ambiguous.
    • Products are license-protected by default. A product is treated as public only when its meta _woonoow_licensing_enabled is explicitly set to string 'no'. Missing, unconfigured, or invalid licensing metadata is treated as protected.
  3. Active website identity pair: Protected products require an active license and an active installation binding:
    text
    installation_id (persistent UUID) + site_url (normalized host domain)
    
    Both components are mandatory. Omitting either returns 400 missing_identity; supplying a complete identity that has no active activation for the license returns 403 domain_not_activated. See Website Identity for identity construction rules.
  4. Server-side release selection: The server determines the single best eligible release based on publication status, version comparison, physical artifact integrity, and license entitlements. Clients never supply a desired target version, release ID, artifact ID, storage key, or checksum.
  5. Short-lived bearer tokens: Local package delivery produces an ephemeral, single-use bearer token. Plaintext tokens are never stored in the database; only their SHA-256 hashes are persisted with strict server-side bindings.

Endpoints summary

MethodRouteAuth / PermissionPurpose
POST/software/packagePublic (internal license / identity check)Canonical operation for server-selected install, reinstall, or update package issuance.
GET, POST/software/checkPublic (internal license / identity check)Compatibility endpoint for update checkers; distinguishes available vs. eligible updates.
GET, HEAD/software/download?token={token}One-time bearer tokenSingle-use package redemption and streaming from the private vault.
GET/software/changelog?slug={slug}Public (unauthenticated)Public changelog feed for published releases.
GET/software/products/{id}/versions/{id}/external-linkProtected license identity, or authenticated purchaser/admin for public productsGated access route for manual external downloads.

Canonical package request (POST /software/package)

POST /software/package is the primary endpoint for retrieving software packages for installation, reinstallation, and updates.

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

Allowed request fields

The server enforces strict JSON schema validation. Any unknown or unsupported parameter causes immediate rejection with HTTP 400 unsupported_parameter.

FieldTypeRequiredDescription
slugstringYesUnique software slug assigned to the parent WooCommerce product.
purposestringYesExactly one of: install, reinstall, or update.
current_versionstringConditionalActual installed version string. Required when purpose is update. Must be omitted when purpose is install or reinstall. Core ordering uses PHP version_compare().
license_keystringProtected productsActive product license key. Omitted for explicitly public products.
site_urlstringProtected productsClient site URL or origin. Normalized by the server to its host domain.
installation_idstring (UUID)Protected productsPersistent canonical installation UUID of the client website.

Field constraints and validation rules

  • Strict rejection of unknown fields: Parameters such as version, release_id, download_url, storage_driver, storage_key, file_path, artifact_id, or checksum are rejected immediately.
  • Install and Reinstall: The current_version field must not be sent. If present (even empty or null), the server returns 400 current_version_not_allowed.
  • Update: The current_version field is mandatory and must be non-empty. If omitted, the server returns 400 current_version_required.
  • Protected products: When licensing is enabled for the product, omitting license_key, site_url, or installation_id returns 400 missing_identity or 403 license_required.

Server release selection algorithm

The server queries published releases for the parent product ordered by:

  1. Current primary release flag (is_current DESC)
  2. Effective publication timestamp (COALESCE(published_at, release_date) DESC)
  3. Version record ID (id DESC)

A candidate release must satisfy all of the following:

  • It belongs to the resolved parent product.
  • Its status is published (never draft or withdrawn).
  • For update, its version compares newer than current_version using PHP version_compare().
  • Its artifact is valid: a local package must resolve to a supported protected filesystem artifact with matching SHA-256; an external release must contain a valid manual HTTPS record and complete SHA-256 metadata.
  • For protected products, the license entitlement rules authorize the release for the requested purpose via Entitlement Evaluation.

Outcome of candidate evaluation:

  • Candidate authorized: The newest release satisfying all constraints is issued.
  • No candidates match (404 no_eligible_release): Returned when no published releases exist for the product, or when for purpose=update no published release is newer than current_version.
  • Candidates rejected (403 no_eligible_release): Returned when published candidate releases exist, but every candidate failed release authorization (e.g., published after update cutoff without history access) or artifact validation. When a candidate fails entitlement authorization, the specific authorization error (e.g. release_not_entitled) is returned.

Request examples

1. Install request (new deployment)

current_version is omitted. The server returns the first artifact-valid, entitled release in its authoritative current/publication ordering.

json
{
  "slug": "woonoow-commerce-pro",
  "purpose": "install",
  "license_key": "WNOW-7F9B-4D2A-881C",
  "site_url": "https://client-store.com",
  "installation_id": "a6b8c9d0-1234-4567-89ab-cdef01234567"
}

2. Reinstall request (repair or replace an installation)

current_version is omitted. The server selects the first published release authorized by the same entitlement/cutoff rules as every other purpose. Because the client does not identify its installed version, reinstall does not promise to return those exact previous bytes.

json
{
  "slug": "woonoow-commerce-pro",
  "purpose": "reinstall",
  "license_key": "WNOW-7F9B-4D2A-881C",
  "site_url": "https://client-store.com",
  "installation_id": "a6b8c9d0-1234-4567-89ab-cdef01234567"
}

3. Update request (upgrade to newer version)

current_version is mandatory. The server selects the first authorized published release that compares newer under PHP version_compare(). Semantic Versioning (for example, 1.4.2) is recommended for predictable publisher/client conventions, but core does not apply a strict SemVer 2.0 regex.

json
{
  "slug": "woonoow-commerce-pro",
  "purpose": "update",
  "current_version": "1.4.2",
  "license_key": "WNOW-7F9B-4D2A-881C",
  "site_url": "https://client-store.com",
  "installation_id": "a6b8c9d0-1234-4567-89ab-cdef01234567"
}

4. Explicitly public product request (unlicensed)

For products configured with licensing disabled, identity and license keys are omitted.

json
{
  "slug": "woonoow-free-starter",
  "purpose": "install"
}

Successful responses

Local vault package response (automated delivery)

When the eligible release is hosted in the verified local vault, the server creates a single-use bearer token and returns an automated download URL.

json
{
  "success": true,
  "purpose": "update",
  "product": {
    "id": 1042,
    "name": "WooNooW Commerce Pro",
    "slug": "woonoow-commerce-pro"
  },
  "version_id": 88,
  "version": "2.0.0",
  "artifact_download_id": "art_99bc81",
  "file_name": "woonoow-commerce-pro_v_2.0.0.zip",
  "file_size": 26214400,
  "file_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "signature": null,
  "signing_key_id": null,
  "signed_at": null,
  "release_date": "2026-09-07 14:30:00",
  "manual_download_only": false,
  "download_url": "https://your-store.com/wp-json/woonoow/v1/software/download?token=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "token_expires_at": "2026-09-07 14:35:00",
  "token_binding_version": 1,
  "entitlement": {
    "license_required": true,
    "license_active": true,
    "update_entitled": true,
    "support_active": null,
    "usage_expires_at": "2027-09-01 00:00:00",
    "updates_expires_at": "2027-09-01 00:00:00",
    "support_expires_at": null,
    "policy_mode": "explicit",
    "historical_downloads": true
  }
}
Response fieldTypeDescription
successbooleanAlways true on successful package issuance.
purposestringEchoes requested purpose (install, reinstall, or update).
productobjectParent product metadata (id, name, slug).
version_idintegerInternal release record ID.
versionstringVersion string of the selected package; ordering uses PHP version_compare().
artifact_download_idstringImmutable artifact binding identifier (UUIDv4 for direct uploads, or WooCommerce download ID for legacy migrated releases).
file_namestringSafe target package filename generated from template.
file_sizeintegerExact file size in bytes.
file_sha256string64-character lowercase hexadecimal SHA-256 hash of the artifact.
signaturestring or nullCryptographic signature metadata (nullable; releases are published unsigned).
signing_key_idstring or nullIdentifier of signing key (nullable in P0).
signed_atstring or nullUTC timestamp of signing (nullable in P0).
release_datestringEffective UTC publication timestamp.
manual_download_onlybooleanfalse for automated local delivery; true for external links.
download_urlstring or nullEphemeral one-time redemption URL. Populated only for local packages.
token_expires_atstring or nullUTC expiration timestamp for the bearer token (default 5 minutes).
token_binding_versioninteger or nullServer token binding schema version (1).
entitlementobjectSnapshot of client entitlement rights evaluated during resolution.

External release response (manual download only)

When the eligible release is configured with an external storage driver, automated updater token generation is prohibited. The response signals manual handling and supplies a gated external route:

json
{
  "success": true,
  "purpose": "update",
  "product": {
    "id": 1042,
    "name": "WooNooW Commerce Pro",
    "slug": "woonoow-commerce-pro"
  },
  "version_id": 89,
  "version": "2.1.0",
  "artifact_download_id": "art_ext_44",
  "file_name": "woonoow-commerce-pro_v_2.1.0.zip",
  "file_size": 28416000,
  "file_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "signature": null,
  "signing_key_id": null,
  "signed_at": null,
  "release_date": "2026-09-08 10:00:00",
  "manual_download_only": true,
  "download_url": null,
  "token_expires_at": null,
  "token_binding_version": null,
  "external_link_url": "https://your-store.com/wp-json/woonoow/v1/software/products/1042/versions/89/external-link",
  "entitlement": {
    "license_required": true,
    "license_active": true,
    "update_entitled": true,
    "support_active": null,
    "usage_expires_at": "2027-09-01 00:00:00",
    "updates_expires_at": "2027-09-01 00:00:00",
    "support_expires_at": null,
    "policy_mode": "explicit",
    "historical_downloads": true
  }
}

Update compatibility check (GET|POST /software/check)

/software/check is a compatibility endpoint designed for WordPress updater libraries (such as Plugin Update Checker). It evaluates whether an update is available and whether the client is eligible to receive it.

http
GET  /wp-json/woonoow/v1/software/check?slug=...&version=...
POST /wp-json/woonoow/v1/software/check
Content-Type: application/json

Request fields

ParameterLocationRequiredDescription
slugQuery / BodyYesSoftware slug of the parent product.
versionQuery / BodyYesActual currently installed version string. Do not pass fake versions like 0.0.0 for new installations; use /software/package instead.
license_keyQuery / BodyProtected productsProduct license key.
site_urlQuery / BodyProtected productsClient site URL (normalized to host domain).
installation_idQuery / BodyProtected productsPersistent canonical installation UUID.

Available vs. eligible semantics

The response makes a strict distinction between releases that exist and releases this client is authorized to download:

  • available: A boolean indicating whether a newer published, artifact-valid release exists in the release stream, regardless of this license's entitlement status.
  • eligible: A boolean indicating whether the client's license and website identity are authorized to receive that newer release.
  • update_available: A legacy boolean field deliberately aligned to equal eligible. It is true only when an authorized delivery path exists.
  • latest_version: Reports eligible_version when the license is entitled; otherwise falls back to available_version; otherwise the current or configured version.
  • eligibility_reason: Explains why an available release cannot be downloaded (e.g., release_not_entitled).

Representative response (update available, license not entitled)

In this scenario, version 2.0.0 exists, but the customer's update access cutoff expired before that version was published:

json
{
  "success": true,
  "update_available": false,
  "available": true,
  "eligible": false,
  "update_eligible": false,
  "product": {
    "id": 1042,
    "name": "WooNooW Commerce Pro",
    "slug": "woonoow-commerce-pro"
  },
  "current_version": "1.4.0",
  "latest_version": "2.0.0",
  "available_version": "2.0.0",
  "eligible_version": null,
  "eligibility_reason": "release_not_entitled",
  "changelog": "Major 2.0 release with rebuilt data structures.",
  "release_date": "2026-09-07 12:00:00",
  "file_size": 26214400,
  "file_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "signature": null,
  "signing_key_id": null,
  "signed_at": null,
  "download_url": null,
  "manual_download_only": false,
  "entitlement": {
    "license_required": true,
    "license_active": true,
    "update_entitled": false,
    "support_active": null,
    "usage_expires_at": "2027-09-01 00:00:00",
    "updates_expires_at": "2026-08-01 00:00:00",
    "support_expires_at": null,
    "policy_mode": "explicit",
    "historical_downloads": true
  },
  "changelog_url": "https://your-store.com/wp-json/woonoow/v1/software/changelog?slug=woonoow-commerce-pro"
}

If the client were eligible for a local package, download_url, token_expires_at, and token_binding_version would be populated with single-use bearer token details identical to POST /software/package.


Download token redemption (GET /software/download)

Clients redeem short-lived bearer tokens via GET /software/download.

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

Security and token binding model

  • Bearer secret: The token query parameter carries a random 64-character bearer string.
  • Hash-only database storage: The store database holds only the SHA-256 hash (token_hash) of the token. Plaintext tokens cannot be recovered from server storage.
  • Strict identity and release binding (Token Binding v1): Every token binds binding_version, protected/public policy, product, release/version, artifact ID, artifact SHA-256, purpose, creation time, and expiry. A protected token additionally binds the exact license, activation ID, activation generation/revision, canonical identity key, installation UUID, and normalized domain. A public-product token has no license or activation identity and cannot be repurposed for another product or release.

Revalidation and atomic one-time claim

When a token is presented for redemption:

  1. Hash lookup: The server hashes the provided token and loads the matching row.
  2. Pre-claim revalidation: Core validates token expiration, invalidation flags, and binding completeness.
  3. Artifact pre-check: Core resolves the authoritative supported artifact and verifies its immutable metadata before entering the transaction. A managed local release must remain in the private vault outside every checked public root with matching size and SHA-256. Legacy wc_downloadable compatibility is accepted only when the resolved file is already outside all checked public roots; addon drivers must provide their own valid metadata contract.
  4. Authenticated encrypted preflight (before transaction): For encrypted-at-rest releases, core executes a bounded-memory preflight verification (64 KB chunk hash loop with a discard sink) before opening the database transaction or acquiring InnoDB row locks (FOR UPDATE). Because Sodium Secretstream authenticates strictly when pulling ciphertext chunks, this preflight verifies the database key, Poly1305 chunk MACs, final tag, and uncorrupted plaintext SHA-256 before any token or license row is locked. Moving this outside the transaction prevents long row locks on large packages. If preflight fails, the request fails closed immediately without consuming the token or streaming broken plaintext with a valid Content-Length.
  5. Short transactional lock: Within a short database transaction, core locks the download, release, and—when protected—license and activation rows (FOR UPDATE). It confirms:
    • The product is published and software distribution is enabled.
    • The licensing policy has not changed since token issuance (license_policy_changed).
    • The license remains valid and entitled to the release.
    • The bound activation remains active with identical generation and revision (activation_binding_changed).
    • Physical file stat stability (mtime and size). While file stat checks detect modification, standard filesystem TOCTOU boundaries remain a hosting consideration if external processes modify files between stat and stream.
  6. Atomic claim: Core executes an atomic conditional SQL update:
    sql
    UPDATE wp_woonoow_software_downloads
    SET used_at = %s
    WHERE id = %d
      AND binding_version = 1
      AND used_at IS NULL
      AND invalidated_at IS NULL
      AND expires_at > %s
    
    If the update affects 0 rows, the claim fails with 403 token_consumed.
  7. Transaction commit before delivery: The transaction commits before any file streaming or extension filter begins.
  8. Post-claim transfer failure: Once claimed, the token is permanently consumed. If a network interruption, connection reset, or post-claim addon error occurs, the token is not reopened. The client must issue a new request to /software/package or /software/check to obtain a fresh token.

Method restrictions and non-consuming behaviors

To protect clients and automated update agents from accidental token consumption:

  • Only token parameter allowed: Any additional or unrecognized query parameter returns HTTP 400 download_override_rejected without consuming the token.
  • HEAD requests are rejected: HTTP HEAD returns 405 head_not_supported, Allow: GET, and Accept-Ranges: none without claiming the token.
  • Byte range requests are rejected: Any request containing a Range header returns 416 range_not_supported and Accept-Ranges: none without claiming the token.

Delivery response headers

When streaming a local package, the server emits:

http
HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="woonoow-commerce-pro_v_2.0.0.zip"
Content-Length: 26214400
Accept-Ranges: none
X-Package-Sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Cache-Control: no-cache, must-revalidate
Pragma: no-cache
Expires: 0

Client rules

  • Do not cache download URLs: Download URLs are single-use and expire quickly (default TTL is 5 minutes, configurable from 1 to 60 minutes). Clients must request package URLs immediately prior to download.
  • Always verify SHA-256: Treat package metadata file_sha256 as the expected value, compute SHA-256 over all downloaded bytes, and compare before extraction or execution. When X-Package-Sha256 is present, it must also match the metadata; the header does not replace byte verification.

Public changelog (GET /software/changelog)

The changelog route exposes version history for a software product.

http
GET /wp-json/woonoow/v1/software/changelog?slug={slug}
GET /wp-json/woonoow/v1/software/changelog?slug={slug}&version={version}

Query parameters

ParameterRequiredDescription
slugYesParent product software slug.
versionNoExact stored release version string. When omitted, all published versions are returned.

Behavior and visibility

  • Published releases only: Draft releases (release_status = 'draft') and withdrawn releases (release_status = 'withdrawn') are excluded.
  • Unauthenticated access: This route is publicly accessible without licensing or identity parameters.
  • Module state note: In the current implementation, /software/changelog is registered as a public informational route; it remains accessible even if the software distribution module is toggled off and does not consume rate-limit tokens.

Response example (version list)

json
{
  "slug": "woonoow-commerce-pro",
  "versions": [
    {
      "version": "2.0.0",
      "release_date": "2026-09-07 14:30:00",
      "changelog": {
        "narrative": "Major release with performance overhauls.",
        "points": [
          { "type": "feature", "text": "Added support for high-throughput order queues." },
          { "type": "fix", "text": "Resolved license transient race condition." }
        ]
      },
      "download_count": 142,
      "file_size": 26214400,
      "file_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "signature": null,
      "signing_key_id": null
    }
  ]
}

Gated external manual releases

For products hosted on external storage (such as third-party vendor platforms or external mirrors), WooNooW provides a gated entitlement route:

http
GET /wp-json/woonoow/v1/software/products/{product_id}/versions/{version_id}/external-link

Query parameters

ParameterRequiredDescription
license_keyProtected productsActive product license key.
site_urlProtected productsWebsite site URL / domain.
installation_idProtected productsPersistent installation UUID.
purposeNoinstall, reinstall, or update (defaults to reinstall).

Authorization rules

  • Protected products: The endpoint requires full identity validation (installation_id + normalized domain matching an active activation) and verifies that the license entitlement allows the release for the specified purpose.
  • Public products: Protected against arbitrary public scraping: access requires either administrative privileges (manage_woocommerce) or a logged-in customer account verified to have purchased the product via wc_customer_bought_product().
  • Operational security boundary: Once the external HTTPS URL is returned to the client, byte transfer, link revocation, access control, and CDN/storage ACLs are outside WooNooW core's control. External links are manual-only and are strictly prohibited from the automated /software/download token route (updater_external_link_prohibited).

Successful response

json
{
  "success": true,
  "download_url": "https://releases.external-vendor.com/builds/pkg-2.0.0.zip",
  "version": "2.0.0",
  "file_name": "pkg-2.0.0.zip",
  "file_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "purpose": "reinstall",
  "manual_download": true
}

Release administration API

These routes are for the WooNooW store, release tooling, and CI automation—not for distributed client software. Every route in this section requires a WordPress user with manage_woocommerce.

For an interactive browser/SPA request, authenticate with the normal WordPress login cookie and X-WP-Nonce. For remote automation, use a WordPress-supported authenticated mechanism such as an Application Password over HTTPS. Keep administrator credentials out of the software package and customer installations.

Administration routes

MethodRoutePurpose
GET/software/products/{product_id}/versionsRead parent product software config and all release states, including drafts and withdrawn releases.
POST/software/products/{product_id}/versionsPublish a release via direct multipart ZIP upload (default) or legacy WooCommerce artifact / external link JSON payload.
PUT/software/products/{product_id}/versions/{version_id}Update changelog/current state; existing version and artifact bindings remain immutable.
POST/software/products/{product_id}/versions/{version_id}/withdrawWithdraw a release and invalidate its unclaimed tokens.
POST/software/products/{product_id}/versions/{version_id}/republishRevalidate and republish a draft or withdrawn release.
GET/software/storage/statusInspect REST-safe vault/document-root diagnostics.
POST/software/products/{product_id}/configUpdate slug, filename template, storage driver, licensing policy, or software-enabled policy.
POST/software/products/{product_id}/artifacts/protectCopy an approved local source into the vault; this is an advanced staging/remediation operation.
POST/software/products/{product_id}/versions/{version_id}/delete-public-sourceExplicitly remove a verified public source for an existing draft/release.
POST/software/products/{product_id}/artifacts/validateCompare a filename/version pair with the effective filename template.

Direct multipart ZIP upload (POST /software/products/{product_id}/versions)

The primary and default release publication mechanism is a direct multipart form-data upload. Operators and deployment pipelines submit the .zip archive directly to the release endpoint. WooNooW stages the package in private storage, computes hashes, and publishes the release immediately into the secure vault without touching the public Media Library.

http
POST /wp-json/woonoow/v1/software/products/{product_id}/versions
Content-Type: multipart/form-data
X-WP-Nonce: <rest-nonce>

Request parameters (multipart/form-data)

ParameterTypeRequiredDescription
filefile / binaryYesThe release package archive. Must have a .zip extension (or an extension permitted by the woonoow/software/allowed_artifact_extensions filter).
versionstringYesSemantic version string (e.g. 1.2.0). Version ordering uses PHP version_compare().
storage_driverstringNoStorage driver selection: "local" (private outside-webroot vault), "local_encrypted" (encrypted-at-rest storage), or omitted (automatic: selects private vault if viable, falls back to encrypted-at-rest).
changelogstring or JSONNoEither a plain narrative string or a JSON-encoded object containing narrative (string) and points (array of { type: string, text: string }).
set_currentstring or booleanNoWhether to designate this version as the active current release. Accepts "true" or "false". Defaults to "true".
filenamestringNoOptional custom target filename override. If provided, it is sanitized against path traversal and validated against the product's filename template pattern. If omitted, WooNooW renders the filename using the product's template.

Backend ingestion & integrity guarantees

  • No WooCommerce Artifact ID Required: Direct upload does not require creating a WooCommerce downloadable file entry or looking up an artifact_download_id. The server automatically generates a unique UUIDv4 identifier (artifact_download_id) upon ingestion.
  • Automatic SHA-256 Checksum, Filename, & Vaulting: The server streams the staged package into {vaultDir}/products/{productId}/{targetFilename}, calculating the SHA-256 hash and byte size in-flight.
  • Master Client File Unaffected, PHP Temp Consumed: The operator's master archive on their local computer is completely untouched. In the server environment, PHP places the upload in temporary storage ($_FILES['file']['tmp_name']). WooNooW verifies is_uploaded_file(), stages it into private vault staging ({vaultDir}/staging/{uuid}.zip) via move_uploaded_file() (which safely consumes PHP's temporary file), applies safe permissions (0640), verifies generic ZIP integrity, copies exclusively into the product vault, and unlinks the staging file immediately.
  • Generic ZIP Structure & Integrity Validation: WooNooW validates ZIP archive integrity by verifying magic byte signatures (PK\x03\x04, PK\x05\x06, PK\x07\x08) and testing archive readability via ZipArchive::open(). The validator supports any generic opaque ZIP packages (WordPress plugins, themes, desktop applications, CLI binaries, or arbitrary archives). It does not inspect or enforce internal WordPress plugin headers or require specific PHP files inside the archive.
  • Staging Cleanup & Ingested Artifact Retention on Failure: Temporary files in {vaultDir}/staging/ are always removed immediately after vault ingestion or upon failure. If release publication fails (whether pre-draft on duplicate versions or mid-transaction rollbacks), the ingested artifact in {vaultDir}/products/{productId}/ is preserved in private vault storage rather than eagerly deleted. This eliminates concurrent deletion races where simultaneous uploads could destroy files in flight, guarantees safe draft recovery, and allows subsequent retries to reuse the verified package safely. Error payloads return storage_key and recovery_action without exposing absolute server filesystem paths.
  • Signature: Optional, Nullable, Still NOT Input Supported: The database schema includes signature (text, nullable), signing_key_id (varchar, nullable), and signed_at (datetime, nullable), and API responses include these fields (defaulting to null). However, neither the direct multipart upload endpoint nor the Admin UI currently provides input fields or supports passing cryptographic signatures or signing keys during version publication. Releases are published unsigned with null signature metadata.

cURL request example

bash
curl -X POST \
  -H "X-WP-Nonce: <rest-nonce>" \
  -F "file=@/path/to/my-plugin-v2.0.0.zip" \
  -F "version=2.0.0" \
  -F "set_current=true" \
  -F 'changelog={"narrative":"Compatibility and performance release.","points":[{"type":"ADD","text":"Direct ZIP upload support."},{"type":"FIX","text":"Resolved vault permissions."}]}' \
  "https://your-store.com/wp-json/woonoow/v1/software/products/1042/versions"

Success response (HTTP 201 Created)

json
{
  "success": true,
  "version_id": 88,
  "message": "Version added successfully"
}

Error responses

StatusError CodeCause / Resolution
400missing_versionThe version parameter was empty or omitted.
400missing_fileNo file parameter was uploaded, or UPLOAD_ERR_NO_FILE occurred.
400upload_file_too_largeThe file exceeds PHP upload_max_filesize or post_max_size.
400upload_partialThe file was only partially received by PHP.
400upload_errorGeneric PHP file upload failure (UPLOAD_ERR_*).
400invalid_uploadUploaded filename is empty or contains null bytes (\0).
400source_extension_not_allowedFile extension is not in the allowed list (default: .zip).
400invalid_source_pathTemporary upload path is invalid or contains null characters.
400source_file_missingTemporary uploaded file is missing or unreadable on the server.
400invalid_upload_sourceTemporary file was not uploaded via a valid HTTP request (is_uploaded_file() check).
400invalid_file_sizeUploaded package file is empty (0 bytes).
400malformed_zip_packageFile lacks valid ZIP magic headers (PK\x03\x04, PK\x05\x06, PK\x07\x08) or fails ZipArchive integrity verification.
400invalid_artifact_filenameTarget filename is empty after sanitization.
400artifact_extension_mismatchTarget filename extension does not match package archive type (.zip).
400filename_template_mismatchCustom filename override does not match the product's filename template pattern.
403source_symlink_rejectedTemporary upload path or staging directory is a symbolic link.
403source_inside_vaultSource temporary file is already inside the vault directory.
404product_not_foundTarget product ID does not exist or is not a WooCommerce product.
409version_existsA release with this version string already exists for this product.
409artifact_name_conflictAn artifact with the target filename already exists in the product vault with different content bytes.
500document_root_unverifiedServer document root could not be determined. Configure WOONOOW_SOFTWARE_DOCUMENT_ROOT.
500insecure_storage_locationVault directory is located inside a public webroot.
500vault_symlink_rejectedThe configured vault path is a symbolic link.
500storage_errorFailed to create vault or staging directory.
500storage_directory_unwritableProtected vault directory is not writable.
500unsafe_storage_permissionsCould not apply safe permissions (0750 / 0640).
500upload_staging_failedFailed to move uploaded file into private vault staging directory.
500copy_artifact_failedFailed to copy staged file into the product vault.
500encryption_key_missingEncrypted releases exist in the database, but the server encryption key is missing. Key must be restored from database backup or WOONOOW_SOFTWARE_ENCRYPTION_KEY.
500encryption_key_corruptedServer encryption key in database or configuration is invalid (not 32 bytes).
500sodium_unsupportedPHP sodium extension with secretstream support (ext-sodium) is missing on the server.
500artifact_corruptedEncrypted artifact chunk failed Poly1305 MAC authentication check (file is corrupted or tampered with), or contains trailing data after final tag.
500artifact_truncatedEncrypted artifact file was truncated or cut short before final authentication tag.

Legacy publication & migration (POST /software/products/{product_id}/versions with JSON)

When migrating an existing release package already uploaded to WordPress Media or configured as a native WooCommerce downloadable file, submit an application/json payload referencing the WooCommerce artifact_download_id:

http
POST /wp-json/woonoow/v1/software/products/1042/versions
Content-Type: application/json
X-WP-Nonce: <rest-nonce>
json
{
  "version": "2.0.0",
  "set_current": true,
  "artifact_download_id": "woocommerce-download-id",
  "storage_driver": "local",
  "delete_public_source": true,
  "changelog": {
    "narrative": "Compatibility and maintenance release.",
    "points": [
      { "type": "ADD", "text": "Added the new integration workflow." },
      { "type": "FIX", "text": "Fixed activation recovery." }
    ]
  }
}

Core resolves the authoritative WooCommerce artifact, copies and hashes the bytes into the product-scoped vault, writes a durable non-current draft, detaches matching native WooCommerce downloads across the parent/variation scope, safely removes the exact public source when authorized, revalidates the vault artifact, and transactionally publishes it. set_current defaults to true; send false to publish without changing the current release.

Shared references block explicit deletion

A public Media/uploads source without deletion consent fails with 409 source_still_public and remains untouched. If deletion is authorized (delete_public_source: true), WooNooW audits known references. If another WooCommerce product, variation, software release, or WordPress post still references the public file, deletion is strictly blocked:

json
{
  "success": false,
  "error": "source_has_shared_references",
  "message": "The public source was preserved because other content or software releases still reference it.",
  "data": {
    "status": 409,
    "draft_version_id": 88,
    "recovery_action": "review_release_draft",
    "references": [
      "Product #1050 downloadable file",
      "Software Version #42"
    ]
  }
}

The public file remains intact, detached downloads are restored, and a recoverable draft is preserved. Operators can resolve the references and call republish on the saved draft.


Publish an external manual release

For packages hosted on external HTTPS infrastructure (vendor CDNs, external mirrors), submit an application/json payload with storage_driver: "external" and the pre-computed SHA-256 hash:

json
{
  "version": "2.0.0",
  "set_current": true,
  "artifact_download_id": "external-download-id",
  "storage_driver": "external",
  "file_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "changelog": {
    "narrative": "External manual release.",
    "points": []
  }
}

Publication detaches the matching native WooCommerce link so it cannot bypass the WooNooW gate. The resulting release is always manual-only: it never receives /software/download updater tokens.


Resolving legacy artifact IDs

Resolving a WooCommerce download_id is required only for the legacy migration workflow and external links. Direct ZIP uploads do not use this step.

An authenticated product response exposes IDs at:

text
GET /wp-json/woonoow/v1/products/{product_id}

simple/parent artifact: downloads[].id
variation artifact:     variations[].downloads[].id
bash
curl --user "release-admin:application-password" \
  "https://your-store.com/wp-json/woonoow/v1/products/1042"

Read downloads[].id or variations[].downloads[].id from the JSON response. Do not log the Application Password or reuse administrator credentials in distributed client software.

Withdraw, recover, and edit

Withdraw a published release:

http
POST /wp-json/woonoow/v1/software/products/1042/versions/88/withdraw
Content-Type: application/json

{ "reason": "Security withdrawal" }

Republish a withdrawn release or recover a draft:

http
POST /wp-json/woonoow/v1/software/products/1042/versions/88/republish
Content-Type: application/json

{ "set_current": true }

A withdrawn release preserves its original effective publication cutoff when republished, so republishing cannot extend customer entitlement. Previously invalidated tokens remain invalid. A draft receives its first publication timestamp only after successful publication.

PUT /versions/{version_id} currently requires the existing version value and may update changelog/current state. It cannot change a published version string, artifact ID, or checksum; publish a new version instead.

Private Artifact Library API

The Private Artifact Library decouples package ingestion from release publication. Store administrators can upload private packages in advance, audit byte size and SHA-256 integrity, inspect which releases currently reference each package, and publish new versions by selecting a previously uploaded artifact without re-uploading bytes.

Packages in the private artifact library are strictly scoped to the parent product's private vault storage and are never exposed in the WordPress Media Library or accessible via public webroot URLs.

List private artifacts

http
GET /wp-json/woonoow/v1/software/products/{product_id}/artifacts
Authorization: Bearer <token> (or WordPress Admin Session / Application Password)

Query parameters:

ParameterTypeDefaultDescription
searchstring""Filter by original filename or artifact UUID
pageinteger11-based page number
per_pageinteger20Results per page (maximum 100)
orderbystring"created_at"Field to order by (created_at, original_filename, file_size)
orderstring"desc"Sort direction (asc or desc)

Response (HTTP 200):

json
{
  "success": true,
  "product_id": 1042,
  "total": 2,
  "pages": 1,
  "page": 1,
  "per_page": 20,
  "artifacts": [
    {
      "id": 15,
      "artifact_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "artifact_download_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "product_id": 1042,
      "original_filename": "MyPlugin-v2.1.0-build.42.zip",
      "file_size": 3456789,
      "file_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "storage_driver": "local",
      "is_protected": 1,
      "created_at": "2026-09-12 10:00:00",
      "usage_versions": ["2.1.0"]
    },
    {
      "id": 16,
      "artifact_id": "c7a8b9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
      "artifact_download_id": "c7a8b9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
      "product_id": 1042,
      "original_filename": "MyPlugin-v2.2.0-rc1.zip",
      "file_size": 3512345,
      "file_sha256": "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
      "storage_driver": "local",
      "is_protected": 1,
      "created_at": "2026-09-12 11:30:00",
      "usage_versions": []
    }
  ]
}

Upload package to private library

http
POST /wp-json/woonoow/v1/software/products/{product_id}/artifacts
Content-Type: multipart/form-data

file: <binary ZIP archive>
storage_driver: local (optional, 'local' or 'local_encrypted')

Response (HTTP 201 Created):

json
{
  "success": true,
  "artifact": {
    "id": 16,
    "artifact_id": "c7a8b9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
    "artifact_download_id": "c7a8b9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
    "product_id": 1042,
    "original_filename": "MyPlugin-v2.2.0-rc1.zip",
    "file_size": 3512345,
    "file_sha256": "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
    "storage_driver": "local",
    "is_protected": 1,
    "created_at": "2026-09-12 11:30:00",
    "usage_versions": []
  },
  "message": "Artifact uploaded and saved to private library successfully."
}

Publish release from private library artifact

When publishing a software release from a previously uploaded private artifact, provide the server-authoritative artifact_id:

http
POST /wp-json/woonoow/v1/software/products/{product_id}/versions
Content-Type: application/json

{
  "version": "2.2.0",
  "artifact_id": "c7a8b9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
  "custom_filename": "",
  "set_current": true,
  "changelog": {
    "narrative": "Release candidate 1 for upcoming 2.2 series",
    "points": [
      { "type": "ADD", "text": "Product-scoped private artifact library" }
    ]
  }
}

Security & Integrity Guarantees:

  • Server-authoritative binding: The server resolves storage_key, file_size, and file_sha256 strictly from the durable database record. Any client-provided storage_key is ignored.
  • Strict product scope enforcement: Artifacts uploaded for Product A cannot be selected or published for Product B. Cross-product requests are rejected with HTTP 403 cross_product_artifact_prohibited.
  • Immutable hash binding: Before creating the release, the server verifies that the physical archive in vault storage matches the recorded SHA-256 hash and size. If the file on disk has been corrupted or altered, publication fails closed with HTTP 409 artifact_integrity_failed.
  • Template independence: The original uploaded package filename (e.g. MyPlugin-v2.2.0-rc1.zip) is preserved in the library record, while the client-facing release binary filename is rendered from the product's filename template (e.g. myplugin_v_2.2.0.zip).
  • Reusable across versions: A single library package can be selected for multiple release versions (e.g. initial release and metadata-only hotfix updates). Each version maintains its own immutable release binding.

Software Artifact Deletion & Storage Management API (P0)

The Artifact Deletion API gives store administrators complete, audited control over physical file removal. It supports deleting unused packages, historical release packages, current release packages, and shared physical packages.

Core Security & Architecture Rules:

  • Authentication & Capabilities: All endpoints require manage_woocommerce. Product-scoped routes additionally require current_user_can('edit_post', $product_id). For browser session requests, a valid WordPress REST nonce (X-WP-Nonce) is mandatory.
  • Target Identification by ID Only: Deletion targets must be identified strictly by server-authoritative database integer IDs or UUIDs (artifact_ids, release_ids). Request payloads that pass raw filesystem paths, vault directories, driver names, or bucket names are strictly rejected with HTTP 400 invalid_parameters.
  • Zero Server Path Leakage & Storage Key Retention: All responses pass through recursive sanitization. Server-internal absolute paths (physical_path, vault_root, storage_root, absolute_path, staging_path, local_path) and private storage identity objects are stripped before returning data to clients. Relative storage_key is preserved in baseline artifact and version publication success responses per existing API contract.
  • Two-Phase State Machine & Idempotency: Deletion executes in two decoupled phases via database journal tables (woonoow_software_deletion_jobs and woonoow_software_deletion_items). The interactive API commits in-transaction closures and enqueues asynchronous physical unlinks, returning HTTP 202 Accepted while unfinished. Repeated execution requests with the same idempotency_key return existing job details. Idempotency survives preview expiry; a committed operation remains replayable even after the preview transient has expired.
  • Publish Prohibition: Artifacts in delete_pending, delete_failed, or deleted state are barred from being bound or published in new releases (409 artifact_not_available).
  • Roadmap Scope: P0 (implemented; automated verification recorded TESTING_CHECKLIST; live host acceptance pending) covers manual deletion, storage usage summary, and impact preview. P1 deduplication, P2 scheduled retention, and cloud storage addons remain future milestones.

1. Storage usage summary

http
GET /wp-json/woonoow/v1/software/storage/usage
GET /wp-json/woonoow/v1/software/storage/usage?product_id=1042
Authorization: Bearer <token> (or WordPress Admin Session / Application Password)

Query parameters:

ParameterTypeDefaultDescription
product_idinteger(all)Scope storage summary to a single WooCommerce product ID
storage_driverstring(all)Scope storage summary to a specific storage driver (local, local_encrypted)

Response (HTTP 200 OK):

json
{
  "measured_at": "2026-09-14T11:00:00Z",
  "source": "database_cached",
  "scope": "product_1042",
  "stored_bytes": 145829120,
  "unused_bytes": 1000000,
  "historical_bytes": 20000000,
  "current_shared_bytes": 124829120,
  "pending_bytes": 0,
  "failed_bytes": 0,
  "deleted_bytes": 5000000,
  "object_count": 6,
  "unknown_size_objects": 0,
  "measurement": "known"
}

Measurement confidence:

  • known: all physical objects have verified exact stored_size_bytes
  • estimated: some objects fallback to legacy catalog file_size
  • mixed: some objects have unknown sizes where neither stored bytes nor file size are available

2. Generate deletion impact preview

Before executing physical deletion, clients must request an impact preview. The preview computes the closure of all affected releases and physical files, returning a short-lived preview_token (valid for 15 minutes).

http
POST /wp-json/woonoow/v1/software/products/{product_id}/artifact-deletions/preview
Content-Type: application/json

{
  "artifact_ids": [15, "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"],
  "release_ids": [42]
}

Validation constraints:

  • At least one target (artifact_ids or release_ids) is required.
  • Combined target count must not exceed 100 per request (max_targets_exceeded).
  • Unknown parameters (such as path, file, force, driver) are rejected with 400 invalid_parameters.
  • Cross-product targets: If target physical artifacts are shared across multiple products, the actor must have edit_post permission on all affected products. If permission is missing on any affected product, preview fails with 403 cross_product_permission_denied. Cross-product deletion is allowed with all affected products consent/permissions (cross_product_impact), not an absolute prohibition. (Note: for library artifact selection during release publication POST /versions, selecting a file uploaded to a different product is rejected with 403 cross_product_artifact_prohibited).

Response (HTTP 200 OK):

json
{
  "success": true,
  "preview_token": "prev_a1b2c3d4e5f60718293a",
  "expires_at": "2026-09-14 11:15:00",
  "product_id": 1042,
  "targets_count": 2,
  "unique_files_count": 1,
  "estimated_removable_bytes": 3456789,
  "releases_affected": [
    {
      "release_id": 42,
      "version": "2.1.0",
      "release_status": "published",
      "is_current": true
    }
  ],
  "impact_summary": {
    "has_current_release": true,
    "has_historical_releases": true,
    "has_shared_artifacts": false,
    "is_last_product_artifact": false,
    "unclaimed_tokens_count": 2
  },
  "required_acknowledgements": [
    "acknowledge_historical_access_loss",
    "acknowledge_current_version_removal"
  ]
}

3. Execute confirmed artifact deletion

http
POST /wp-json/woonoow/v1/software/products/{product_id}/artifact-deletions
Content-Type: application/json

{
  "preview_token": "prev_a1b2c3d4e5f60718293a",
  "idempotency_key": "idem_7f8e9d0c1b2a3456",
  "acknowledgements": [
    "acknowledge_historical_access_loss",
    "acknowledge_current_version_removal"
  ]
}

Execution semantics:

  • Stale preview protection: If targets, release references, or file metadata changed between preview creation and execution, returns 409 deletion_preview_stale.
  • Tiered acknowledgements: All strings in required_acknowledgements must be provided in acknowledgements. If any acknowledgement is missing, returns 400 deletion_confirmation_required.
  • Idempotency survives preview expiry: Repeated execution with matching idempotency_key, product, and preview token returns the committed operation without re-initiating unlinks, even after the preview transient has expired or been consumed.
  • In-Transaction phase: Sets target artifacts to delete_pending, marks referencing published releases withdrawn, clears is_current if applicable, and transactionally invalidates outstanding unclaimed tokens with reason artifact_deleted.
  • Asynchronous worker execution: Enqueues physical file unlinks to background workers. Returns HTTP 202 Accepted while status is pending or running, or HTTP 200 OK if immediately completed.

Response (HTTP 202 Accepted):

json
{
  "success": true,
  "operation_id": "op_9876543210abcdef",
  "status": "pending",
  "product_id": 1042,
  "items_count": 1,
  "summary": {
    "total_items": 1,
    "completed_items": 0,
    "failed_items": 0,
    "removed_bytes": 0
  }
}

4. Get deletion operation status

http
GET /wp-json/woonoow/v1/software/artifact-deletions/{operation_id}

Response (HTTP 200 OK):

json
{
  "success": true,
  "operation_id": "op_9876543210abcdef",
  "status": "completed",
  "created_at": "2026-09-14 11:00:15",
  "completed_at": "2026-09-14 11:00:18",
  "summary": {
    "total_items": 1,
    "completed_items": 1,
    "failed_items": 0,
    "removed_bytes": 3456789
  },
  "items": [
    {
      "item_id": 204,
      "artifact_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "status": "deleted",
      "removed_bytes": 3456789,
      "error_code": null,
      "attempts": 1
    }
  ]
}

5. Retry failed deletion operation

Re-enqueues items in failed status or pending status with outcome = 'retryable_failure' within an existing deletion job for worker processing. Resets item status to pending, clears error codes and outcomes, resets attempts to 0, and dispatches background workers immediately. Background workers apply exponential backoff (min(300, 2^attempts * 30) seconds) for retryable failures.

http
POST /wp-json/woonoow/v1/software/artifact-deletions/{operation_id}/retry

Response (HTTP 202 Accepted):

json
{
  "success": true,
  "operation_id": "op_9876543210abcdef",
  "status": "pending",
  "retried_items_count": 1,
  "message": "Failed deletion items re-queued for processing."
}

Storage adapter extension hooks contract

Core natively implements local and local_encrypted drivers. Addon storage drivers (such as future Cloudflare R2 or Amazon S3 integrations) must adhere to the following filter contracts:

  1. woonoow/software/storage_capabilities: Addons must declare their inspection and deletion capabilities:
    php
    add_filter('woonoow/software/storage_capabilities', function ($caps, $driver, $artifact) {
        if ($driver === 'r2') {
            return ['stat' => true, 'delete' => true];
        }
        return $caps;
    }, 10, 3);
    
  2. woonoow/software/stat_managed_artifact: Must return a validated snapshot array containing identity (driver, backend, key), identity_hash, boolean exists, stored_size_bytes (integer ≥ 0 when existing; null when absent), absence_evidence (required when exists === false), and capabilities. Core strictly rejects raw HTTP 4xx/5xx status codes and failure keywords (timeout, curl_error, permission_denied, etc.) with 500 storage_stat_failed. Addon drivers must normalize authorized 404 responses to named strings: not_found or confirmed_absent only.
  3. woonoow/software/delete_managed_artifact: Must return an outcome array confirming physical absence:
    • outcome: Exactly one of 'deleted', 'already_missing', 'retryable_failure', 'permanent_failure'.
    • removed_bytes: Integer bytes removed (must be 0 for already_missing and delete markers).
    • error_code: Redacted machine-readable error string if failed.

Verified Post-Stat Identity Absence: For both deleted and already_missing outcomes from addon drivers, core performs a mandatory post-stat verification (SoftwareStorage::stat_managed_artifact) to confirm physical absence (exists === false), matching backend identity, and version. Delete markers and missing files report 0 for removed_bytes (delete markers do not count as released bytes). If the file remains or only a delete marker was created, core returns retryable_failure with error code file_remains_after_delete or delete_marker_only.

No Core Cloud Support: Cloudflare R2 and Amazon S3 are not implemented in WooNooW core. All cloud functionality belongs in future dedicated addons.


Product configuration and storage checks

POST /software/products/{product_id}/config accepts slug, filename_template, storage_driver, licensing_enabled, and software_enabled. A licensing/software policy change invalidates outstanding tokens for that product. The default filename template is {slug}_v_{version}.zip; clients must not derive a target URL or version from it.

Call GET /software/storage/status before local publication. The endpoint reports effective_driver (local or local_encrypted), storage_mode (private_vault or encrypted_at_rest), and detailed diagnostics for both storage types. When storage_mode: "encrypted_at_rest" is active, encrypted.status: "secure" confirms that authenticated encryption (Sodium Secretstream XChaCha20-Poly1305) is initialized with a secure server-managed key. filesystem_status: "secure" means PHP verified the reported filesystem boundaries; overall status: "host_verification_required" remains until the operator audits web-server aliases, CDN/cache copies, and the old source URL. See Software Distribution Configuration for the operational checklist.

POST /artifacts/validate returns HTTP 200 with valid: false when a filename does not match the effective template. Treat the response body—not only the HTTP status—as the validation result.


Rate limiting architecture

Package/check issuance and gated external-link requests use an application-level rate limiter to reduce brute-force abuse and polling loops.

  • Storage mechanism: Backed by WordPress transients with a 1-minute TTL (woonoow_software_rate_{hash}).
  • Bucket segmentation:
    • Protected requests: Keyed by authoritative database license_id (license|{license_id}).
    • Public products: Keyed by product ID and hashed IP (public|{product_id}|{ip_hash}).
    • Unknown products or failed license lookups: Keyed by hashed client IP (unknown|{ip_hash}).
    • Hashed-IP guard: An overarching client IP bucket capped at max(10, min(500, limit * 5)).
  • Limits: Derived from the software module setting rate_limit (clamped between 1 and 100 requests per minute; default is 10).
  • Best-effort boundary: The limiter uses a non-atomic get_transient → increment → set_transient sequence. Under high concurrency, requests may race past the nominal threshold. It is an abuse mitigation measure, not a cryptographic or host security boundary. Production deployments must enforce edge/reverse-proxy rate limits (e.g., Nginx limit_req, Cloudflare, or AWS WAF).
  • Exceeded response: When a bucket limit is reached, the server returns HTTP 429 rate_limited with error data containing retry_after: 60.

Integrity and cryptographic signing

  • Managed-local integrity: A publishable local release carries a complete 64-character lowercase hexadecimal SHA-256 value. Core calculates and verifies the actual bytes during vault staging and before token claim/redemption.
  • External integrity metadata: External publication requires the administrator to supply a valid SHA-256 value, but core does not fetch or continuously verify third-party bytes. Once the external URL is disclosed, provider behavior and byte delivery are outside core; the receiving workflow should verify the downloaded bytes against the published hash.
  • Signing metadata status: The API returns signature, signing_key_id, and signed_at fields in package and update responses for forward compatibility. These fields are optional and nullable in database records and API responses (defaulting to null). WooNooW core does not require cryptographic signatures, and neither the direct upload endpoint nor the Admin UI currently provides input fields or support for entering signatures or signing keys. If non-null signature data is present via custom backend filters, it must not be treated as an authoritative verification mechanism without external validation.

Explicit system limitations

Client developers and integrators must be aware of the following deliberate architectural constraints:

  1. No byte-range resumption: Partial downloads, multi-threaded download managers, and Range headers are explicitly unsupported and return HTTP 416 range_not_supported.
  2. No HEAD probing: Automated tools must not send HEAD requests to verify token validity; HEAD returns HTTP 405 head_not_supported.
  3. No client version selection: Clients cannot request an arbitrary target version (for example, "give me 1.8.0"). The server selects a release based on status, entitlement, configured ordering, and—for update requests—PHP version_compare() against the installed version.
  4. No provider integration in core: WooNooW core has no native Cloudflare R2, AWS S3, Google Drive, or OneDrive driver. Core supports managed local delivery and unmanaged HTTPS external links for manual download only.
  5. Addon extension boundary: A storage addon may extend accepted drivers and artifact metadata through woonoow/software/valid_storage_drivers and woonoow/software/resolve_artifact_metadata, then prepare delivery through woonoow/software/prepare_download. The preparation hook runs strictly after the core atomic token claim commits. Adding a driver name alone does not implement validation or delivery, and no addon may bypass product/release authorization, license checks, or token consumption.

Principal error codes

Software routes currently use two error envelopes: standard WordPress REST errors expose the machine code as code, while several compatibility/controller responses expose it as error. Normalize with const errorCode = body.code ?? body.error, then branch on that stable value plus the HTTP status. Never match localized message text.

Errors are grouped below by lifecycle phase: Software Preflight Errors, Release Candidate & Authorization Errors, and Download Token & Redemption Errors.

1. Software preflight errors

Preflight errors are evaluated before candidate release resolution begins. They indicate invalid input syntax, rate limits, disabled modules, or failure of the requesting identity and base license to pass prerequisite checks.

Error codeHTTPEndpoint(s)Meaning and required client action
module_disabled503/software/package, /software/check, /software/download, external-link routeSoftware Distribution module is disabled on the vendor store.
missing_params400/software/package, /software/checkRequired package fields (slug, purpose) or compatibility-check fields (slug, version) were omitted.
missing_slug400/software/changelogThe changelog request omitted slug.
unsupported_parameter400/software/packageThe request JSON contained unexpected parameters (e.g., version or path overrides). Clean request payload.
invalid_purpose400/software/package, /software/.../external-linkpurpose must be strictly install, reinstall, or update.
current_version_required400/software/package, /software/checkPurpose update requires a non-empty current_version string.
current_version_not_allowed400/software/packagecurrent_version was sent for install or reinstall. Remove the key from the JSON payload.
rate_limited / rate_limit_exceeded429/software/package, /software/check, external-link routeDistribution rate limit reached. Inspect data.retry_after (seconds) and back off.
product_not_found404/software/package, /software/check, /software/changelogNo published WooCommerce product matches the requested slug.
software_slug_ambiguous409/software/package, /software/check, /software/changelogMultiple published products share the same software slug. Store admin must resolve duplicate slugs.
software_disabled403/software/package, /software/check, /software/download, external-link routeDistribution is disabled for this product (_woonoow_software_enabled is not yes).
licensing_unavailable503/software/package, /software/check, /software/download, external-link routeProduct requires a license but the Software Licensing module is disabled. Never falls back to public access.
license_required403/software/package, /software/checkProduct requires a license, but license_key was omitted.
missing_identity400/software/package, /software/check, external-link routeProtected request omitted installation_id or site_url.
invalid_identity400/software/package, /software/check, external-link routeInstallation UUID is malformed or site URL cannot be normalized to a valid host.
invalid_license403/software/package, /software/check, /software/downloadLicense key does not exist or has been deleted.
license_product_mismatch403/software/package, /software/checkThe license key belongs to a different WooCommerce product.
license_inactive / revoked403/software/package, /software/check, /software/download, external-link routeBase license status is revoked or inactive.
license_expired / expired403/software/package, /software/check, /software/download, external-link routeLicense usage expiration has passed.
subscription_inactive403/software/package, /software/check, /software/download, external-link routeLinked WooNooW native subscription is lapsed, cancelled, or inactive.
invalid_usage_expiry403/software/package, /software/checkLicense usage expiry timestamp format is unparseable.
domain_not_activated403/software/package, /software/check, external-link routeThe exact installation_id and normalized domain pair has no active activation for this license.

2. Release candidate & authorization errors

Evaluated during server release selection (resolve_eligible_release()) and entitlement evaluation (LicenseEntitlements::authorize_release()).

Error codeHTTPEndpoint(s)Meaning and required client action
no_eligible_release403/software/packagePublished candidate releases exist for the product, but none satisfy release authorization or artifact validation (e.g. all newer releases fall outside the update window without historical access).
no_eligible_release404/software/packageNo published candidate releases match the request at all (e.g. no published releases exist, or for update no release is newer than current_version).
release_not_entitled403/software/package, /software/check eligibility reason, /software/download, external-link routeThe candidate release publication date falls outside the license's updates_expires_at cutoff and historical downloads are not permitted.
release_product_not_entitled403/software/package, /software/downloadThe release does not belong to the parent product authorized by the license.
license_usage_inactive403/software/package, /software/downloadEvaluated license usage right is inactive during candidate release authorization.
invalid_release_purpose400/software/packageRelease authorization called with purpose other than install, reinstall, or update.
invalid_release_date500/software/package, /software/downloadStored release publication date is missing or malformed in the database.
release_not_found / version_not_found404/software/download, /software/changelog, external-link routeRequested or bound release record does not exist in the database.
cross_product_artifact_prohibited403/software/products/{id}/versions, /software/products/{id}/artifacts/{id}Selected library artifact belongs to a different product vault. Cross-product usage is strictly blocked.
unprotected_artifact_prohibited403/software/products/{id}/versionsArtifact in library is marked unprotected or public legacy source and cannot be used for releases.
release_not_published410/software/download, /software/.../external-linkRelease is in draft status and cannot be delivered.
release_withdrawn410/software/download, /software/.../external-linkRelease was withdrawn by an administrator; all outstanding tokens for it are invalidated.
artifact_not_found404/software/package, /software/downloadThe bound vault archive file is missing from disk. Token is not claimed.
artifact_integrity_missing500/software/package, /software/downloadRequired SHA-256 metadata is missing or malformed for the release.
artifact_integrity_failed500/software/package, /software/downloadPhysical artifact file hash does not match the database checksum. Token is not claimed.
source_still_public409/software/package, /software/downloadLocal release cannot be served while a public source file remains unprotected.
unsupported_storage_driver501/software/package, /software/downloadRelease uses a storage driver unsupported by core.
updater_external_link_prohibited403/software/downloadAn external release link was presented to the local download token route.
not_external_version400/software/.../external-linkExternal link route was called on a release configured for local vault storage.
authentication_required401/software/.../external-linkPublic external download requires WordPress account login.
customer_not_entitled403/software/.../external-linkLogged-in user did not purchase the public external software product.

3. Download token & redemption errors

Evaluated when redeeming a one-time bearer token via GET /software/download?token=....

Error codeHTTPEndpoint(s)Meaning and required client action
download_override_rejected400/software/downloadQuery parameters other than token were sent. Remove all extra parameters.
missing_token400/software/downloadThe token query parameter was empty or omitted.
head_not_supported405/software/downloadHEAD method was used. Use GET to download the package. Non-consuming.
range_not_supported416/software/downloadRequest contained a Range header. Download the full archive in a single GET request. Non-consuming.
invalid_token403/software/downloadBearer token hash was not found in the database.
token_binding_required403/software/downloadLegacy token without binding version 1 schema or incomplete identity binding. Request a new package.
token_expired403/software/downloadBearer token TTL (60 seconds) expired. Request a new package.
token_invalidated403/software/downloadToken was invalidated by deactivation, license revocation, or release withdrawal.
token_consumed403/software/downloadToken has already been used or lost the atomic claim race. Request a new package.
software_product_unavailable403/software/downloadThe bound WooCommerce parent product is no longer published.
activation_inactive403/software/downloadThe installation activation bound to this token was deactivated before redemption.
activation_binding_changed403/software/downloadActivation identity, revision, or generation changed since token issuance. Request a new package.
license_policy_changed403/software/downloadProduct licensing policy changed after token issuance. Request a new package.
release_binding_changed403/software/downloadRelease version, artifact ID, or checksum no longer matches token binding.
artifact_changed409/software/downloadArtifact file size or modification time changed during the authorization transaction.
token_generation_failed / token_write_failed500/software/package, /software/checkDatabase error during download token creation.
invalid_prepared_download500/software/downloadAn extension hook returned an invalid delivery payload after token claim.

4. Storage & artifact deletion errors

Evaluated during artifact inspection, deletion preview, execution, status, retry, and release publication.

Error codeHTTPEndpoint(s)Meaning and required action
deletion_preview_stale409/software/products/{id}/artifact-deletionsTargets, release references, or file metadata changed between preview generation and execution. Request a fresh preview.
deletion_confirmation_required400/software/products/{id}/artifact-deletionsRequired explicit impact acknowledgement string missing from execution payload.
artifact_delete_in_progress409/software/products/{id}/artifact-deletionsTarget artifact is already being processed by an active deletion job.
artifact_not_available409/software/products/{id}/versionsAttempted to bind or publish a release using an artifact in delete_pending, delete_failed, or deleted state.
cross_product_permission_denied403/software/products/{id}/artifact-deletions/preview, /software/products/{id}/artifact-deletionsActor lacks edit_post permission on one or more products affected by a shared artifact deletion.
cross_product_artifact_prohibited403/software/products/{id}/versionsArtifact target belongs to a different product vault scope (e.g. attempting to bind an artifact from Product A into Product B).
storage_delete_unsupported501/software/products/{id}/artifact-deletionsStorage driver does not declare delete capability or no delete handler is registered.
storage_stat_unsupported501/software/products/{id}/artifact-deletions/previewStorage driver does not declare stat capability or no stat handler is registered.
storage_delete_failed500/software/artifact-deletions/{op}/retryPhysical file unlinking failed or absence could not be confirmed.
invalid_driver_stat_contract500/software/products/{id}/artifact-deletions/previewAddon storage driver returned a malformed stat snapshot structure.
invalid_driver_delete_contract500/software/artifact-deletions/{op}Addon storage driver returned an invalid deletion outcome.
invalid_parameters400/software/products/{id}/artifact-deletions/preview, /software/products/{id}/artifact-deletionsRequest payload contained unrecognized parameters (e.g. paths, storage keys, or force flags). Targets must be specified via IDs only.
missing_targets400/software/products/{id}/artifact-deletions/previewNeither artifact_ids nor release_ids was provided.
max_targets_exceeded400/software/products/{id}/artifact-deletions/previewPreview request exceeded the limit of 100 targets.

Last updated Sep 8, 2026