Verify a license
POST /api/integrations/v1/licenses/verifyCall this endpoint from your backend before granting new access to your licensed offering — a new session, a new token, or a tool call that is not covered by a recent check.
Identify the license
Section titled “Identify the license”Use the canonical licenceKey delivered at issuance in the licence.issued
event. Store it exactly as received and treat it as opaque. For NFT-backed licenses it has the form
chainId:contractAddress:tokenId:
{ "licenceKey": "11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42" }Do not create your own license ID, and do not substitute an order ID, requestId or transactionId.
Request
Section titled “Request”curl --request POST "$LICENCHAIN_API_BASE_URL/api/integrations/v1/licenses/verify" \ --header "Authorization: Bearer $LICENCHAIN_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "licenceKey": "11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42"}'// Server-side only. Returns true only when LicenChain reports valid === true.// Every other outcome (non-200, timeout, malformed body, valid !== true) denies new access.const baseUrl = process.env.LICENCHAIN_API_BASE_URL;const apiKey = process.env.LICENCHAIN_API_KEY;
export async function isLicenceValid(licenceKey: string): Promise<boolean> { const payload = { licenceKey }; try { const response = await fetch(`${baseUrl}/api/integrations/v1/licenses/verify`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), signal: AbortSignal.timeout(5000), }); if (response.status !== 200) return false; const result: { valid?: unknown } = await response.json(); return result.valid === true; } catch { return false; }}
// Example: the licenceKey you stored from the licence.issued event.const allowed = await isLicenceValid('11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42');# Server-side only. Returns True only when LicenChain reports valid is True.# Every other outcome (non-200, timeout, malformed body, valid is not True) denies new access.import osimport requests
base_url = os.environ["LICENCHAIN_API_BASE_URL"]api_key = os.environ["LICENCHAIN_API_KEY"]
def is_licence_valid(licence_key: str) -> bool: payload = {"licenceKey": licence_key} try: response = requests.post( f"{base_url}/api/integrations/v1/licenses/verify", headers={"Authorization": f"Bearer {api_key}"}, json=payload, timeout=5, ) if response.status_code != 200: return False return response.json().get("valid") is True except (requests.RequestException, ValueError): return False
# Example: the licenceKey you stored from the licence.issued event.allowed = is_licence_valid("11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42")Responses
Section titled “Responses”Valid license (illustrative)
Section titled “Valid license (illustrative)”{ "schemaVersion": "1.0", "licenceKey": "11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42", "productRef": "ai-specialist-standard", "buyerOrganizationId": "org_2B5D8F1H4K7M", "licenceHolderId": "org_2B5D8F1H4K7M", "seatLimit": 25, "issued": true, "valid": true, "status": "active", "reason": null, "validFrom": "2027-01-01T00:00:00Z", "validUntil": "2028-01-01T00:00:00Z", "checkedAt": "2027-03-15T10:30:00Z", "revision": 3}Fail closed: lifecycle state missing
Section titled “Fail closed: lifecycle state missing”When LicenChain has issuance evidence but no authoritative current lifecycle state for the license, it fails closed:
{ "schemaVersion": "1.0", "licenceKey": "11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42", "productRef": "ai-specialist-standard", "buyerOrganizationId": "org_2B5D8F1H4K7M", "licenceHolderId": "org_2B5D8F1H4K7M", "seatLimit": 25, "issued": true, "valid": false, "status": "unverified", "reason": "lifecycle_state_missing", "validFrom": null, "validUntil": null, "checkedAt": "2027-03-15T10:30:00Z", "revision": null}Expired license
Section titled “Expired license”{ "schemaVersion": "1.0", "licenceKey": "11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42", "productRef": "ai-specialist-standard", "buyerOrganizationId": "org_2B5D8F1H4K7M", "licenceHolderId": "org_2B5D8F1H4K7M", "seatLimit": 25, "issued": true, "valid": false, "status": "expired", "reason": "validity_period_ended", "validFrom": "2027-01-01T00:00:00Z", "validUntil": "2028-01-01T00:00:00Z", "checkedAt": "2028-01-02T08:00:00Z", "revision": 4}Response fields
Section titled “Response fields”| Field | Description |
|---|---|
valid |
The only field that may permit access (license half). true only when status is active and checkedAt is inside the validity period. |
issued |
Issuance evidence exists for this license. This is licensing evidence, not proof that the license is currently active. |
productRef |
Registered product associated with the issued corporate license. |
buyerOrganizationId |
LicenChain ID of the purchasing Buyer organization. |
licenceHolderId |
LicenChain ID of the organization holding the license rights; this may differ from the Buyer. |
seatLimit |
Seat or unit allowance inside this one corporate license, or null when not applicable. Your delivery system enforces named-user assignments and limits. |
status |
active, not_yet_valid, expired, suspended, revoked or unverified. |
reason |
null when valid. Otherwise lifecycle_state_missing, validity_period_not_started, validity_period_ended, suspended or revoked. New values may be added — treat unknown values as deny. |
validFrom |
Start of the license period (UTC, inclusive), or null if unknown. |
validUntil |
End of the license period (UTC, exclusive), or null if unknown. |
checkedAt |
When LicenChain evaluated the state (UTC). |
revision |
Monotonic lifecycle revision, or null if unknown. Higher revisions supersede lower ones. |
Product, Buyer, holder and seat-limit references come from the issued license record and are returned
only within the authenticated Seller’s scope. They may still be known when current lifecycle status
is unverified: that does not make the license valid. These fields are informational and never
replace your own customer, seat and user-authorization checks.
Decision table
Section titled “Decision table”| Outcome | New access |
|---|---|
200 and valid === true |
Allowed, if your own user checks also pass |
200 and valid === false (any status) |
Deny |
404 licence_not_found (unknown, or belongs to another Seller) |
Deny |
400, 401, 403 |
Deny and alert — this is an integration fault |
429, 503, timeout, network error |
Deny new access; retry later |
Results are never cached by LicenChain (Cache-Control: no-store). If you cache a positive result to
reduce latency, keep the cache short, key it by licenceKey, never cache a positive result past
validUntil, and never extend it after a failed check. Lifecycle changes such as suspension or
revocation are reflected on the next successful check; no propagation time is guaranteed in this
target contract.
Errors
Section titled “Errors”{ "type": "https://developers.licenchain.io/api/docs/errors/#licence_not_found", "title": "License not found", "status": 404, "code": "licence_not_found", "detail": "No license with this licenceKey is visible to your Seller account.", "traceId": "trc_4H8K2M6Q9R"}See Errors & retries for the full list.
Privacy
Section titled “Privacy”The request contains only the canonical licenceKey. The response includes opaque organization
identifiers, not Buyer names or contact emails. Treat those IDs as confidential metadata and never
expose them to an AI agent or unauthorized Seller. Do not log API keys. Logging licenceKey,
status, reason and traceId is sufficient for routine troubleshooting.
Reference
Section titled “Reference”- API reference —
verifyLicence - MCP & agent access control