Resolve License
Resolve a license key to a single, authoritative "entitled right now" answer — the valid flag, subscription status, the currently-granted allowed_features, and any grace window — in one call. This folds together entitlement checks and status/expiry validation, and is the recommended endpoint for a runtime capability gate.
Reading a custom entitlement value (e.g. an
updates_untildate)? Use Validate License instead — it returns the full entitlements map.resolvereturns only granted feature codes (entitlements whose value is booleantrue), not arbitrary fields.
Endpoint
POST /api/v1/integration/licensing/resolveHeaders
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | Your API key |
Content-Type | Yes | application/json |
X-Request-ID | Recommended | UUID for tracing (echoed in meta.request_id) |
Request Body
{
"license_key": "VX-ACME-abc123def456",
"features": ["analytics", "sso"]
}| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | Full license key. Must belong to the tenant resolved from your API key. |
features | string[] | No | If supplied, allowed_features is the intersection with what the license actually grants. Omit (or send empty) to return all granted features. |
Response (200 OK)
{
"data": {
"valid": true,
"status": "active",
"allowed_features": ["analytics", "sso"],
"grace_period_ends_at": null,
"expires_at": null,
"license": {
"id": "01HXYZABCDEFGHJKMNPQRSTVWX",
"key": "VX-ACME-abc123def456",
"type": "perpetual"
}
},
"meta": {
"request_id": "uuid",
"api_version": "1"
}
}| Field | Type | Description |
|---|---|---|
data.valid | boolean | true iff the license is currently usable (active, trialing, or canceled-inside-grace). Authoritative — do not re-derive on the client. |
data.status | string | One of active, trialing, past_due, paused, canceled, expired, suspended, revoked (US spelling). |
data.allowed_features | string[] | Currently-granted feature codes (entitlements whose value is boolean true). [] when nothing is granted. Filtered by request features[] if supplied. |
data.grace_period_ends_at | string | null | ISO-8601 UTC. Set only in a grace state (canceled/past_due with grace remaining) — "when access drops to Free". Otherwise null. |
data.expires_at | string | null | ISO-8601 UTC. The license/period expiry; null for perpetual keys. Distinct from grace_period_ends_at. |
data.license.id | string | Internal ID (ULID). Stable — use for support/webhook cross-references. |
data.license.key | string | Echo of the input key. |
data.license.type | string | perpetual, subscription, or trial. |
Adapter rule (single line): if (!data.valid) drop_to_free(). Status drives optional UX (a grace banner, a "trial ended" prompt) but never gates capability.
Status state machine
| State | valid | status | grace_period_ends_at | allowed_features |
|---|---|---|---|---|
| Subscription active, not expired | true | active | null | full set |
| Subscription trialing, trial live | true | trialing | null | full set |
| Past due, inside dunning grace | true | past_due | grace end | full set |
| Past due, past grace | false | past_due | null | [] |
| Paused | false | paused | null | [] |
Canceled (cancel_at_period_end), inside period | true | canceled | period end | full set |
| Canceled, period ended | false | canceled | null | [] |
License expires_at past (e.g. a lapsed trial) | false | expired | null | [] |
| License revoked | 404 (see errors) | — | — | — |
| Tenant suspended | 403 (see errors) | — | — | — |
Grace policy: canceled → remaining time to current_period_end; past_due → Stripe-default dunning (~21 days), grace_period_ends_at = last-retry deadline; paused → no grace. valid + grace_period_ends_at are authoritative — you do not model dunning yourself.
Error Responses
| Code | HTTP | Description |
|---|---|---|
AUTH.INVALID_API_KEY | 401 | Invalid or missing API key |
TENANT.STATUS.SUSPENDED | 403 | Tenant is suspended |
LICENSE.NOT_FOUND | 404 | Key does not exist, belongs to another tenant, or is revoked (deliberately indistinguishable) |
| (validation) | 422 | e.g. license_key missing, features[] contains a non-string |
RATE_LIMIT.EXCEEDED | 429 | Rate limit exceeded — honour Retry-After |
INTERNAL.UNEXPECTED | 5xx | Server fault — retry with backoff |
Notes
- Writes a
LICENSE.RESOLVEDaudit event and updates the license'slast_used_at. - Caching: cache the response per
license_keyfor ~5 minutes. On anyvalid: false, evict and drop to Free. OnLICENSE.NOT_FOUND/TENANT.STATUS.SUSPENDED, evict and do not retry. Keep any offline cache as a "ValidonX-unreachable" fallback only —grace_period_ends_atis the authoritative grace signal. resolveis additive. The path-1 endpoints — Validate License and Check Entitlements — remain available; reach for Validate specifically when you need the full entitlements map (e.g. a customupdates_untilfield).