License Entitlements

Independent usage, update, support, and release access rights with immutable issuance snapshots

Overview

WooNooW separates the core software licensing lifecycle from software update rights and customer support rights. A customer may possess an active license that permits running the software indefinitely, while their access to new releases or customer support operates on an independent schedule.

text
License Lifecycle (Usage) ≠ Software Update Rights ≠ Support Rights

When a qualifying WooCommerce order completes (or an administrator creates a license), WooNooW resolves the catalog entitlement policy configured on the purchased product or variation and records an immutable entitlement snapshot directly into the license record. Subsequent changes to catalog settings apply only to newly issued licenses; existing license snapshots remain unchanged.


Public entitlement fields

The software distribution endpoints (/software/check and /software/package) return this compact entitlement object:

FieldTypeDescription
license_requiredbooleanWhether software distribution requires a license (false for public software, true for protected software).
license_activebooleanEffective use permission to activate and run the software. Evaluates administrative status (active vs revoked), usage expiry (expires_at), and subscription status if linked to a WooNooW native subscription (woonoow_subscriptions).
update_entitledbooleanWhether the license is currently authorized to receive software updates.
support_activeboolean or nullWhether customer support rights are active. null means unspecified/not tracked, not active or inactive.
usage_expires_atstring or nullUTC datetime (Y-m-d H:i:s) when product usage rights expire, or null if lifetime/unbounded.
updates_expires_atstring or nullUTC datetime (Y-m-d H:i:s) when software update rights end, or null if unbounded.
support_expires_atstring or nullUTC datetime (Y-m-d H:i:s) when customer support rights end, or null if unbounded or unspecified.
policy_modestring or nullexplicit for a structured snapshot, legacy for an empty legacy snapshot, missing for a fail-closed incomplete license read, or null for public products.
historical_downloadsbooleanWhether an active license may download releases published on or before updates_expires_at after the update window ends.

The licensing validation endpoint (/licenses/validate) returns top-level product_id, variation_id, usage_expires_at_utc (canonical RFC 3339 UTC string, e.g. 2027-09-08T12:00:00Z), alongside expires_at (SQL datetime). Its nested entitlements object includes the evaluated rights above plus usage_expires_at_utc, history_access, updates_mode, support_mode, usage_expiry_valid, policy_valid, policy_error, issuance timestamp (issued_at), and WooNooW native subscription telemetry (subscription_status, subscription_active, subscription_linked).


Explicit policy structure

A catalog entitlement policy defines rules across three dimensions:

json
{
  "updates": {
    "mode": "days",
    "days": 365
  },
  "support": {
    "mode": "days",
    "days": 90
  },
  "history_access": true
}

Updates policy (updates)

The updates object is required:

ModeAllowed parametersDescription
licenseNoneUpdate rights follow the license usage window (updates_expires_at matches usage_expires_at).
daysdays (positive integer)Updates expire days days after license issuance date (issued_at).
unlimitedNoneUpdates remain entitled as long as the license is active (license_active: true).
noneNoneUpdates are not entitled (update_entitled: false).

Support policy (support)

The support object is optional. If omitted, it defaults to {"mode": "unspecified"}:

ModeAllowed parametersDescription
unspecifiedNoneSupport rights are not tracked (support_active: null, support_expires_at: null).
daysdays (positive integer)Support expires days days after license issuance date (issued_at).
unlimitedNoneSupport remains active as long as the license is active (support_active: true).
noneNoneSupport is explicitly inactive (support_active: false, support_expires_at: null).

Historical access (history_access)

history_access is an optional boolean (default: false). It governs whether a customer whose update window has elapsed may still install, reinstall, or update to a release published on or before the update cutoff:

text
historical_downloads = license_active && history_access && updates.mode !== "none"

Dimension-specific "unlimited"

The unlimited mode applies only to the explicitly selected dimension (updates or support):

  • updates.mode: "unlimited" does not grant a lifetime product usage license, nor does it grant unlimited support.
  • If the license expires (expires_at), is revoked by an administrator, or has an inactive linked subscription, license_active becomes false.
  • When license_active is false, update and support rights are immediately inactive regardless of an unlimited mode setting.

Fail-closed validation

WooNooW enforces strict schema boundaries:

  1. Unknown fields: Supplying unrecognized properties in policy definitions returns invalid_entitlement_policy (HTTP 400).
  2. Invalid parameters: Supplying days on modes other than days, non-integer days, or negative values is rejected.
  3. Snapshot integrity: If a non-empty stored snapshot is malformed or tampered with, runtime evaluation keeps policy_mode: "explicit" but marks policy_valid: false, update_entitled: false, and support_active: false. Malformed data fails closed and is never interpreted as legacy or unlimited.
  4. Incomplete license reads: If the entitlement_policy field is absent from a loaded license row, evaluation returns policy_mode: "missing", policy_valid: false, and policy_error: "invalid_entitlement_snapshot". Usage state is preserved, but update and support rights fail closed until the complete license record/schema is available.

Legacy license behavior

Licenses issued before entitlement policies were introduced (or when no catalog policy is configured) operate in legacy mode:

  • policy_mode is "legacy".
  • update_entitled mirrors license_active.
  • updates_expires_at mirrors usage_expires_at.
  • support_active is null (unspecified).
  • historical_downloads is false.

An empty policy snapshot is never interpreted as an unlimited grant.


Package purposes and release cutoffs

Clients request packages via /software/package or check for updates via /software/check. When requesting a package, the client specifies a purpose:

  • install — First-time installation on a site.
  • reinstall — Repairing or replacing an installation with a server-selected authorized release; it does not identify or promise the exact previously installed bytes.
  • update — Upgrading to a newer release (current_version required).

Purpose cannot bypass update rights

All three package purposes apply the exact same release authorization rules in LicenseEntitlements::authorize_release:

  1. License usage must be active: license_active must be true. An expired, revoked, or subscription-lapsed license cannot download packages under any purpose.
  2. Product matching: The release must belong to the parent product of the license.
  3. Active update window: If update_entitled is true, any published release whose publication timestamp (release_date) is on or before updates_expires_at (or if updates_expires_at is null) is authorized.
  4. Expired update window with historical access: If update_entitled is false, the client may access a release only if historical_downloads is true and the release was published on or before updates_expires_at.
  5. Post-cutoff releases blocked: A client cannot use purpose: "install" or purpose: "reinstall" to acquire releases published after its update window expired. Any release published after updates_expires_at returns release_not_entitled (HTTP 403).
text
Release Publication Date ≤ updates_expires_at
  ├── update_entitled: true   ──> Authorized (install, reinstall, update)
  ├── update_entitled: false
  │     ├── historical_downloads: true   ──> Authorized (install, reinstall, update)
  │     └── historical_downloads: false  ──> 403 release_not_entitled
  └── Release Date > updates_expires_at  ──> 403 release_not_entitled

Policy inheritance and snapshot immutability

WooNooW resolves catalog entitlement policies hierarchically:

text
Variation policy (if configured)
  └── Parent product policy (if configured)
        └── Legacy mode (unconfigured)
  1. Child variation override: A variation can define its own explicit policy, completely overriding the parent product settings.
  2. Inheritance from parent: If a variation has no entitlement policy configured, it inherits the parent product's policy.
  3. Snapshot creation: When a license is issued, WooNooW resolves the effective policy for the specific product_id and variation_id, calculates the exact expiration timestamps based on the license issuance time (issued_at), and stores the result in entitlement_policy.
  4. Catalog Immutability & Subscription Renewal: Once saved to the license record, the snapshot protects customers against retroactive catalog changes: subsequent merchant modifications to product or variation policies govern only newly issued licenses. For linked recurring subscriptions, however, successful renewal payments automatically calculate and advance the entitlement snapshot windows (LicenseManager::renew_entitlement_snapshot()) to grant coverage for the renewed billing cycle.

Merchant Admin REST API

Store administrators manage product entitlement policies through dedicated REST endpoints.

Permission: manage_woocommerce (endpoints are registered only when the Licensing module is enabled). Browser/SPA requests use the normal WordPress cookie plus REST nonce; remote automation can use another WordPress-supported authenticated method, such as an Application Password.

Read policy

http
GET /wp-json/woonoow/v1/licensing/products/{product_id}/entitlement-policy
GET /wp-json/woonoow/v1/licensing/products/{product_id}/entitlement-policy?variation_id={variation_id}

Query parameters

ParameterTypeRequiredDescription
product_idintegerYesParent product ID in route path.
variation_idintegerNoOptional child variation ID. Must belong to product_id.

Configured parent response (200)

json
{
  "product_id": 123,
  "variation_id": 0,
  "configured": true,
  "scope_configured": true,
  "inherited": false,
  "source": "product",
  "source_id": 123,
  "policy": {
    "updates": {
      "mode": "days",
      "days": 365
    },
    "support": {
      "mode": "unspecified"
    },
    "history_access": false
  }
}

Inherited variation response (200)

When querying a variation that has not defined an override, WooNooW indicates inheritance from the parent:

json
{
  "product_id": 123,
  "variation_id": 456,
  "configured": true,
  "scope_configured": false,
  "inherited": true,
  "source": "product",
  "source_id": 123,
  "policy": {
    "updates": {
      "mode": "days",
      "days": 365
    },
    "support": {
      "mode": "unspecified"
    },
    "history_access": false
  }
}

Unconfigured response (200)

When neither the product nor the variation has an entitlement policy:

json
{
  "product_id": 123,
  "variation_id": 0,
  "configured": false,
  "scope_configured": false,
  "inherited": false,
  "source": null,
  "source_id": 0,
  "policy": null
}

Update policy

http
PUT /wp-json/woonoow/v1/licensing/products/{product_id}/entitlement-policy[?variation_id={variation_id}]
Content-Type: application/json

To configure a policy on a specific variation, supply variation_id in the query string. Do not place it inside the JSON policy object: policy validation is strict and rejects unknown fields.

Request body

json
{
  "updates": {
    "mode": "days",
    "days": 365
  },
  "support": {
    "mode": "days",
    "days": 90
  },
  "history_access": true
}

Read-back verification

When saving a policy, WooNooW writes the validated JSON string to post meta (_woonoow_entitlement_policy) and immediately reads it back from the database. It compares the read-back value using hash_equals. If the stored data does not match the encoded policy, the operation aborts and returns entitlement_policy_save_failed (HTTP 500).

Success response (200)

json
{
  "product_id": 123,
  "variation_id": 0,
  "configured": true,
  "scope_configured": true,
  "inherited": false,
  "source": "product",
  "source_id": 123,
  "policy": {
    "updates": {
      "mode": "days",
      "days": 365
    },
    "support": {
      "mode": "days",
      "days": 90
    },
    "history_access": true
  }
}

Error responses

HTTPCodeMeaning
400invalid_entitlement_policyPolicy payload failed schema validation, contained unknown fields, or omitted updates.
400invalid_entitlement_variationvariation_id is invalid or does not belong to product_id.
404entitlement_product_not_foundproduct_id does not exist or is itself a variation rather than a parent product.
500entitlement_policy_save_failedPolicy could not be saved or failed read-back verification.

Last updated Sep 8, 2026