Contract version licensing-management/v1.
This is the public integration surface of the license server: the routes a
Publisher's back office, a fulfillment service and a buyer's agent call, and
the two unauthenticated routes anyone holding a certificate can call.
The RSL Open Licensing Protocol
endpoints are listed in the route table below so a route cannot ship
undocumented, but they are not part of this contract: /token and /key
answer in the Open Licensing Protocol's own shape — {error, error_description} with RSL's own codes — rather than in the envelope
described here, and their codes are not part of it. The same is
true of /introspect, which answers RFC 7662's shape for the same reason: its
caller is a Publisher's Resource Server following the Recommendation, not an
integration that has read this document. Anything AssetRouter adds to one of
their bodies is namespaced.
What this surface is not
It is not a second business path. Every route below is a thin adapter over one
control-plane command, which is where idempotency, the authorization decision,
request fingerprinting and the audit record live. Anything you cannot do
through a command you cannot do here, and there is no administrative bypass.
Base URL and versioning
https://<deployment>/licensing/
The management routes carry v1 in the path. v1 is this document. A change
that would break a caller — a code's meaning, a field's type, a route's
shape — arrives as v2; a new route or a new error code is additive and
ships under v1.
The two public routes carry an environment (sandbox or live) in the path
instead of a version, because they are a property of the issuer being verified
rather than of this contract.
Authentication
HTTP Basic, client_secret_basic, on every management route:
Authorization: Basic base64(rsl_client_id + ":" + client_secret)
Which client_id. The user-id here is the RSL client_id: an opaque
value this server issued at registration — ag_live_… or ag_sb_… for an
agent, a readable slug for an operator's own clients. It is never derived from
a hostname or a URL.
It is not the OAuth Client ID Metadata Document (CIMD) client_id, and the two
cannot be interchanged: CIMD §4.1 forbids pairing its URL identifier with
client_secret_basic or any other shared-secret method, which is the method
this API requires. A URL placed here is read by any conformant parser as the
user-id https and the remainder as the secret (RFC 7617 §2) — an
authentication failure neither specification flags as a misuse, and which this
server answers with the same invalid_client it gives every other failure.
The secret is shown once when it is issued and stored only as a keyed digest.
It begins with the role it belongs to — pub_sk_… for a Grant issuer,
rs_sk_… for a Resource Server, ag_sk_… for an agent — so a value pasted
into the wrong configuration line is visible as such before the server has to
answer role_not_permitted. The prefix is part of the secret and carries no
entropy of its own; a secret issued before prefixes existed stays valid.
Rotation issues a new generation while the previous one keeps working for a
week, so a running integration can be updated without a flag day. The
generation that authenticated a request is recorded on that request's audit
row.
Every way of failing to be an authenticated client of the tenant you addressed
answers the same 401 invalid_client: an unknown or malformed tenant id, an
unknown client, a wrong secret, a suspended registration, a client this tenant
never authorized, and one whose authorization was revoked. They are not told
apart on purpose — distinguishing them would let anyone holding one valid
credential enumerate the tenants on a deployment and the clients inside them.
Tenancy
The tenant is in the path. One client registration may be authorized by
several consenting tenants — that is what lets a buyer's agent hold a single
credential across Publishers — so the request has to say which tenant it is
acting in. What it may do there is never taken from the request: the role
comes from the authorization row that tenant wrote.
Identifiers are scoped to the tenant in the path. An identifier belonging to
another tenant, a malformed one, and one that never existed all answer with
the same 404 and the same body.
Idempotency
Every mutation requires a key:
Idempotency-Key: <your own unique value>
Replaying a key with the same request returns the first outcome, with
"replayed": true and status 200 instead of 201. Replaying it with a
different request answers 409 idempotency_conflict. Keys are namespaced
per client, so two clients of one tenant may use the same key for their own
requests.
One resolution is idempotent on more than its key: POST .../principals is
identified by its alias tuple, so the same customer resolved under a different
key returns the same Principal rather than creating a second one.
Rate and abuse controls
The management and OLP endpoints below do not apply application-level request quotas. Network protections can still limit requests. Your integration should handle network failures and follow the public-endpoint limits described below.
Where capacity is actually protected, and by whom:
| Surface | Who bounds it |
|---|---|
These management routes, and /olp/token, /olp/key, /olp/introspect |
Network-level controls, per source address. |
Whatever reaches your Resource Server, and so /olp/introspect through it |
Your own boundary — a per-IP limit, a token-shape filter, bot management. That traffic arrives here under your identity, so we cannot tell your agents from a stranger's. |
| The four unauthenticated public routes (certificate status, JWKS, and the two RSL document routes) | A per-address tap in this server, described with those routes below. It caps the cost of an unauthenticated database read; it is not an abuse ceiling and no plan raises it. |
A request body larger than 64 KB answers 413 request_too_large before it is
parsed; 415 unsupported_media_type and 400 malformed_json answer the same
way. None of the three costs you anything but the request.
Error model
Every refusal answers with the same shape:
{
"error": "<code from the registry below>",
"description": "<the registry's own sentence>",
"field": "<the offending field, for invalid_request only>"
}
The description comes from the registry, never from a stringified exception.
No error body echoes a value from the request, and no response body on this
surface carries a client secret, a credential digest, or a License Token.
Routes
| Route | Purpose |
|---|---|
GET /licensing/{environment}/.well-known/jwks.json |
Every public key an environment's certificates may be signed under. Public. |
GET /licensing/{environment}/certificates/{certificate_id}/status |
Whether the Grant behind a certificate is good now. Public. |
GET /licensing/{environment}/connect/{link} |
The form a Publisher's customer opens to set up its agent. Public, HTML, and the only surface here that renders one. |
POST /licensing/{environment}/connect/{link} |
Completes that setup and shows the credential once. |
POST /licensing/{environment}/agent-registrations |
Claim an enrolled hostname with its single-use code and receive that registration's only secret. Unauthenticated, because the caller is collecting the credential it does not yet have. |
GET /licensing/{environment}/rsl/scopes/{scope_id} |
The licence document governing a License Scope now. Public; this is what a robots.txt License: line names. |
GET /licensing/{environment}/rsl/{document_id} |
The licence document one publication froze, at its permanent address. Public. |
GET /licensing/{environment}/terms/{terms_id} |
The governing prose a licence document's rows point at, served as an attachment. Public. |
POST /licensing/v1/tenants/{tenant_id}/principals |
Resolve one beneficiary, idempotently on its alias tuple. |
GET /licensing/v1/tenants/{tenant_id}/principals/{principal_id} |
Read one beneficiary snapshot. |
POST /licensing/v1/tenants/{tenant_id}/connect-sessions |
Open a front door for one customer and receive the link to send it. |
GET /licensing/v1/tenants/{tenant_id}/connect-sessions/{session_id} |
Whether that session is pending, redeemed or expired, and how many usable Grants its beneficiary holds. Carries no credential. |
GET /licensing/v1/tenants/{tenant_id}/measurements |
How much this server observed, per customer and scope, every figure carrying its tier label. |
POST /licensing/v1/tenants/{tenant_id}/agent-authorizations |
Turn a registered hostname into a licensed agent for one customer, in one transaction. Contract: agent-authorization. |
POST /licensing/v1/tenants/{tenant_id}/grants |
Issue one Grant through the calling client's own delegation. |
GET /licensing/v1/tenants/{tenant_id}/grants/{grant_id} |
Read one Grant. |
POST /licensing/v1/tenants/{tenant_id}/grants/{grant_id}/renewal |
Reassert an entitlement and create the successor Grant. |
POST /licensing/v1/tenants/{tenant_id}/grants/{grant_id}/revocation |
End a Grant before its window does, and record why. |
GET /licensing/v1/tenants/{tenant_id}/certificates/{certificate_id} |
Retrieve one full License Certificate, including its JWS. |
POST /licensing/v1/tenants/{tenant_id}/license-decisions |
Evaluate one request and record the decision with its evidence. |
POST /licensing/v1/tenants/{tenant_id}/license-decisions/{decision_id}/binding |
Attach an open decision to the external workflow that will fulfil it. |
POST /licensing/v1/tenants/{tenant_id}/license-decisions/{decision_id}/cancellation |
End an open or bound decision. |
GET /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses |
List this scope's rights rows. |
POST /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses |
Write one rights row, creating it or replacing what that path pattern said. |
PUT /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses/{license_id} |
Update one rights row at its own address. |
DELETE /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses/{license_id} |
Remove one rights row. It stops being published at the next publish. |
GET /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/publications |
This scope's publications with their derived end times, and whether the working rows have moved since. |
POST /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/publications |
Render this scope's rights rows into a licence document and publish it. |
POST /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/publications/withdrawal |
Withdraw every publication of this scope, keeping the record of what each served. |
POST /licensing/v1/tenants/{tenant_id}/publications/{publication_id}/restoration |
Copy a past publication's rows back into the working rows. Publishing stays a separate act. |
POST /licensing/v1/tenants/{tenant_id}/olp/token |
RSL OLP. Issue a short-lived License Token. Answers in OLP's shape, not this one. |
POST /licensing/v1/tenants/{tenant_id}/olp/key |
RSL OLP. Present in every profile; the base profile returns 403 insufficient_scope and no key material. |
POST /licensing/v1/tenants/{tenant_id}/olp/introspect |
RSL OLP. Whether a token still permits the resource submitted, which is as far as its Grant reaches. Answers in OLP's shape, not this one. |
Endpoints
POST /licensing/v1/tenants/{tenant_id}/principals
Resolve the beneficiary an external reference names, creating it once.
{
"issuer": "publisher",
"external_subject": "customer-1",
"legal_name": "Northwind Research",
"country": "IE",
"identifier_scheme": "lei",
"identifier_value": "...",
"assurance_level": "publisher_asserted",
"asserting_party": "Atlas Press",
"provenance": "publisher_assertion"
}
issuer, external_subject and legal_name are required. Answers
{"operation", "replayed", "principal"}. Provenance and the resolving actor
are recorded on the first resolution and never re-stamped.
GET /licensing/v1/tenants/{tenant_id}/principals/{principal_id}
Answers {"principal"}. The external subject is never returned: it is the
Publisher's own customer key, and it is deliberately absent from the request
fingerprint for the same reason.
POST /licensing/v1/tenants/{tenant_id}/grants
Issue one durable authorization.
{
"principal": "<principal public id>",
"license_scope": "<license scope public id>",
"source_type": "publisher_entitlement",
"source_ref": "crm:subscription:1",
"entitlement": {"reference": "sub-1001", "period_end": "2026-12-31T00:00:00+00:00"},
"resource": {"submitted_resource": "https://...", "requested_use": "ai-input"},
"payment_type": "subscription",
"pattern": "/reports/",
"declared_possession_digest": ""
}
There is no field naming the Grant issuer binding. The binding is the one
belonging to the authenticated client, so a client cannot act through another
client's broader delegation. A caller holding none answers
403 grant_issuer_binding_required.
entitlement is required: the validity window is derived from it and from
nothing else, so a call without one has no window to issue. It is refused as
invalid_request naming the field, before anything is read.
resource is optional, and supplying it is what chooses the Grant's subject:
a call naming one issues a Grant over that exact resource, and a call omitting
it issues a Grant over the whole License Scope. A resource on an origin no
binding of the scope has asserted is refused resource_outside_license_scope
here as it is on /license-decisions and /token — a Grant issued there would
be active and unusable, since the gate's credential is bound to the asserted
origin. The evidence digest is derived, never accepted.
payment_type and pattern say which offer on which row is being bought.
payment_type is RSL's own <payment type="…"> value — purchase,
subscription, free and the rest of section 3.7's closed vocabulary — and
never an identifier: an offer has no id of its own, being named by its row and
its payment type. The field was called offer until 2026-09-19, which read as
if it took one; pattern names the row, and is needed only for a Grant over
the whole scope, since a Grant over a resource finds its row from the path.
Both are optional: a row selling one thing is unambiguous, and an entitlement
that names neither takes that single offer. A row selling several ways refuses
a call that names no type (offer_not_in_publication) rather than choosing
one, because choosing would mean silently selling the cheapest.
The reach follows from the subject and is never stated twice. A Grant naming a
resource may take an offer of either reach — buying the section and using it
for one article is ordinary — while a Grant over the scope may take only a
region offer, because a purchase sold for one article must not become a
subscription to the section (offer_reach_mismatch).
The Grant freezes a copy of the offer: the row pattern, the payment
declaration exactly as the Publisher wrote it, the reach, the access basis and
the digest of the document it was read from. A renewal re-finds the same offer
by that name on whatever publication is current then, so an ordinary price
change renews at the new price rather than refusing.
The scope must have a current publication (no_current_publication), because
that publication is what the Grant is issued under and what it freezes: the
Grant records it, and its terms are read back from it for as long as the Grant
lives. Publishing again does not move a Grant already issued.
Answers {"operation", "replayed", "grant"}, and the Grant names the
publication it froze and the offer it was issued under. Its state is read from the clock, so a Grant past
its window reports expired even though no sweep wrote that.
GET /licensing/v1/tenants/{tenant_id}/grants/{grant_id}
Answers {"grant"}. Object-scoped: an operator or auditor reads its whole
tenant, a Grant issuer reads what its own delegation covers, and an agent
reads the beneficiary it is bound to. Everyone else is answered as if the
Grant did not exist.
POST /licensing/v1/tenants/{tenant_id}/grants/{grant_id}/renewal
{"entitlement": {"reference": "sub-1002", "period_end": "..."}, "source_ref": ""}
entitlement is required here as on issuance: a renewal is a reassertion,
and one that asserts no period has nothing to renew.
Creates a successor Grant linked to its predecessor, with its own window,
its own evidence snapshot and its own certificate. The predecessor is never
touched. Replaying the key returns the same successor; a genuinely new period
is a new key.
POST /licensing/v1/tenants/{tenant_id}/grants/{grant_id}/revocation
{"reason_category": "subscription_ended", "evidence_reference": ""}
The effective time is now and cannot be supplied. Revocation does not
invalidate the certificate: the certificate remains true evidence that the
right was issued, and the public status route is what reports that the Grant
has ended.
Object-scoped in the same way the read is: an operator ends any Grant in its
tenant, a Grant issuer ends only what its own delegation covers, and one
holding no delegation answers 403 grant_issuer_binding_required. A Grant
under another Licensor of the same tenant is refused by the binding's own
reason, not by an unknown-Grant answer — the caller was told it exists by the
identifier it already holds.
GET /licensing/v1/tenants/{tenant_id}/certificates/{certificate_id}
Answers {"certificate"}, including the compact JWS. Retrieval is
authenticated and object-scoped to the same four parties the Grant read
serves: the Principal's own agent, an authorized client, the applicable
Publisher, and an authorized auditor.
Verification needs none of this. Anyone holding a certificate verifies its
signature offline against the JWKS and asks the public status route whether
the Grant is still good.
POST /licensing/v1/tenants/{tenant_id}/license-decisions
{
"principal": "<principal public id>",
"license_scope": "<license scope public id>",
"resource": {
"submitted_resource": "https://atlas.example/reports/q1",
"requested_use": "search"
}
}
Evaluates one request against the License Scope named and records the answer
with its evidence. All three members are required. The terms it decides
against are the scope's current publication — what the Publisher is telling
the world now — read from this server's own frozen rows and never fetched from
the Publisher's origin. A scope that has published nothing answers
no_current_publication; a resource on a host no origin binding of the scope
has asserted answers resource_outside_license_scope; a path no row of that
publication covers answers no_covering_row.
Answers {"operation", "replayed", "license_decision"}, and the decision names
the publication it was made against.
The non-normative twin of OLP /token: same command, same publication, and
the decision rather than a token comes back.
POST /licensing/v1/tenants/{tenant_id}/license-decisions/{decision_id}/binding
{"external_source_reference": "orders:po-4471", "fulfill_by": "2026-10-01T00:00:00+00:00"}
Attaches a decision awaiting external fulfillment to the workflow that will
provide the evidence. Binding creates no Grant and proves no payment. The same
reference under the same window is the binding that already exists; a
different one answers 409 decision_already_bound. fulfill_by must carry a
UTC offset.
POST /licensing/v1/tenants/{tenant_id}/license-decisions/{decision_id}/cancellation
{"reason": "customer withdrew"}
Final. A fulfilled or already cancelled decision answers
409 decision_not_cancellable.
GET /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses
Answers {"content_licenses"}, one entry per rights row: its path pattern, what
it permits and prohibits, its access basis, its price, its reporting
obligations, whether it names a License Server, and the governing prose it
points at as both a public id and an address. The whole list comes back — a
scope's rows are what one Publisher wrote by hand about one site, and there is
nothing to page through.
POST /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses
{
"pattern": "/reports/",
"terms": "<terms snapshot public id>",
"prohibits": {"usage": ["ai-train"]},
"offers": [
{
"permits": {"usage": ["search", "ai-input"]},
"access_basis": "reusable_fetch",
"reach": "region",
"payment": {"type": "subscription", "amount": {"value": "120.00"}}
},
{
"permits": {"usage": ["search"]},
"access_basis": "single_fetch",
"reach": "exact_resource",
"payment": {"type": "purchase", "amount": {"value": "12.00"}}
}
],
"reporting": [],
"names_license_server": true
}
pattern, terms and offers are required; the rest default to empty, and
names_license_server to true.
Offers are the ways this row may be bought, one per rendered <license>
element. Each carries what it permits, what it costs, how far it reaches once
bought, and what access it brings; reach defaults to region and payment
may be omitted, which is how RSL says a use costs nothing. What is shared
stays on the row — the pattern, the prohibitions, the reporting obligations,
the governing prose — because a prohibition is a property of the content
rather than of the sale.
An offer is named by its payment type and its reach, and a row carries at most
one of each pair (offer_repeats_type_and_reach). That pair is what a Grant
records and what a renewal looks up, so it has to survive a price edit — which
a digest of the payment would not. Two subscriptions over one pattern are
therefore unsayable: RSL carries no duration, so a monthly and an annual are
indistinguishable in the published document, and a Publisher selling both
publishes a row for each over its own path.
The same use may not be free under one offer and paid under another
(free_and_paid_for_one_use): free admission would always resolve that pair
in favour of free, and the paid offer could never be sold. Different uses at
different prices are ordinary — searching free beside training by subscription
is a price list.
A row must carry at least one offer (no_offers). reach outside
region/exact_resource answers unknown_offer_reach; offers sent as
anything but a list answers offers_not_a_list, and a non-object entry
offer_not_a_mapping.
The response carries the offer list and, beside it, permits — the union
across the offers, derived and read-only. It is what the row licenses at all;
which offer grants what is the offers' own answer. Answers
{"operation", "replayed", "content_license"} with 201, whether the row was
written for the first time or replaced: a row is its pattern, so a write
declares what that path licenses rather than creating an object with a life of
its own, and both answers say the same thing about the same address. A replay
of a request already made under one idempotency key answers 200.
An upsert keyed on (scope, pattern), because that is what a row is: posting
a pattern the scope already holds replaces what that row said rather than
adding a second row beside it. A pattern is an RFC 9309 path prefix with an
optional trailing $ and no *; the refusals are pattern_not_rooted,
pattern_has_wildcard, pattern_has_whitespace, pattern_anchor_not_final
and pattern_not_text. A row must permit at least one usage across its offers
(no_permitted_usage) and may not permit and prohibit the same value
(permits_and_prohibits_conflict). A terms id this tenant does not hold
answers unknown_terms, the same answer an unknown one gets.
Editing a row changes nothing that is served. What is served is the scope's
current publication, and it moves when somebody publishes.
PUT /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses/{license_id}
The same body, at the address of one row. The pattern is taken from the row
this address names: leave pattern out, or send the row's own. A body naming a
different pattern is refused with pattern_is_not_the_rows rather than moving
the row, because (scope, pattern) is the row's identity and honouring it would
empty this address and write to another. Answers
{"operation", "replayed", "content_license"} with 200.
The address names both the scope and the row, and the two are checked against
each other: a row addressed through a scope that does not hold it answers
unknown_content_license, exactly as an identifier this tenant does not hold
does.
reporting is a list of strings and names_license_server is a boolean, both
refused with invalid_request naming the field when they are anything else. A
bare string sent for reporting is refused rather than stored one letter per
entry, and "false" is refused rather than read as true.
DELETE /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/licenses/{license_id}
Takes no body. Answers {"operation", "replayed", "license_scope", "pattern"}.
Deleting is an ordinary edit and not a takedown: nothing served changes until
somebody publishes, and every publication that carried this row keeps carrying
it, because the rows are frozen inside it. An identifier this tenant does not
hold answers unknown_content_license, and so does a row addressed through a
scope that does not hold it.
GET /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/publications
Answers {"publications", "unpublished_changes"}. Each publication carries its
start (published_at) and an end derived from its two stamps — ended_at is
the earlier of the supersession and the withdrawal. At most one entry has no
end, and that one is is_current.
unpublished_changes is {"rights", "payment"}, two booleans and not one: a
price edit moves the payment answer and leaves the rights answer alone, which is
what lets a Publisher see at a glance that the rights did not move. Both are
derived from the digests the current publication already carries and neither is
stored. A scope with nothing published reports both true as soon as it has a
row, and both false while it has none.
POST /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/publications
Takes no body. Renders the scope's rights rows into one licence document and
freezes it as a Publication. Each row becomes one <content> element with
its own <license>, so a document says what is licensed per path and at what
price — the price is on the row, not on the publication.
The scope must declare at least one row (no_rows) and must have asserted at
least one host (no_asserted_host), because a document naming no origin rests
on no asserted control. A row whose pattern overlaps or nests inside a pattern
another scope of this tenant currently publishes on the same host is refused
(pattern_overlaps_other_scope): two documents would claim the same paths at
two prices.
Answers {"operation", "replayed", "publication"}. The publication carries
both addresses: current_document_url is what goes into the origin's
robots.txt License: line and never changes again, and document_url is
the permanent per-publication address a token and a certificate name. The
bytes are not in the body — they are what those two addresses serve, and
document_digest is how you check that what you fetched is what was frozen.
rights_digest and payment_digest are separate on purpose: a price edit
moves one and not the other, which is what lets a reader see that a price
changed and no rights did.
Publishing again supersedes the previous publication rather than replacing it.
Nothing served stops being served: every publication keeps its own address and
its own bytes, because a token issued under one outlives it.
This is the step that makes a scope able to license anything: /token and
POST /license-decisions both refuse a scope with no current publication.
POST /licensing/v1/tenants/{tenant_id}/license-scopes/{scope_id}/publications/withdrawal
Takes no body. Withdraws every publication of the scope that is not already
withdrawn, superseded ones included: the scope address answers 410, live
tokens stop being active, and licensing against the scope refuses until it is
published again.
Nothing is deleted, and the stamp is one-way. Each publication's own permanent
address keeps serving the bytes it served — a Grant issued under a publication
outlives it, and the document its terms were stated in has to stay readable.
Withdrawing a scope whose publications are all already withdrawn answers
nothing_to_withdraw.
Addressed at the scope rather than at one publication, because that is what the
act reaches: a withdrawal ends licensing for the scope and stamps every
publication it has.
POST /licensing/v1/tenants/{tenant_id}/publications/{publication_id}/restoration
Takes no body. Copies that publication's frozen rows back into the scope's
working rows, replacing them all — the publication is a complete statement of
what the scope licensed, and merging it into whatever is there now would
produce a row set nobody chose. Answers
{"operation", "replayed", "license_scope", "publication"}.
Addressed at the publication, because choosing which past rows to copy back is
exactly choosing a publication; the scope is read off it rather than named
again, so the two cannot be given as a pair that disagrees.
Publishing stays a separate act (POST .../publications). Until somebody
publishes, what is served is unchanged and the difference shows up as an
unpublished change on the history route. Restoring rows the scope already has
answers rows_already_restored, and a row whose governing prose this tenant no
longer holds answers terms_not_in_tenant.
POST /licensing/v1/tenants/{tenant_id}/olp/token
RSL Open Licensing Protocol, form-encoded, HTTP Basic. Every parameter is the
specification's; nothing of ours has to be learned to make this call:
grant_type=client_credentials&
resource=https://atlas.example/reports/&
license=<the exact <license> element being requested>
resource accepts both readings the standard leaves open. RSL 5.4 defines it
as the url of the governing <content> element — the scope pattern — and
the OLP draft calls it the asset's own URL, required to fall within a scope.
Send either: a value naming a scope selects that scope, and a value inside one
selects the same scope, so both acquire the same licence.
license is required and carries the request. The use being asked for is read
from its <permits type="usage"> declaration, where RSL puts it. One use per
request: a declaration naming more than one is refused
(several_usages_requested) rather than resolved, because a Grant and the
token in front of it record one requested use — issuing for all of them would
widen a Grant on the strength of a list nobody compared. A client that wants
two asks twice. A licence declaring no usage at all is refused
(usage_not_requested).
The submitted licence must be the governing document's terms or narrower
it may drop usage permissions, and may not add one, drop a prohibition
or replace the payment declaration. In practice a client sends back what it
read, keeping the one use it wants.
Answers 200 with {access_token, token_type: "License", expires_in, ...}
and namespaced identifiers for the token, the decision, and the Grant where
there is one — a free policy governed by this server creates none, so that
identifier is absent and the decision is what a caller follows. The raw
token value is returned once and stored nowhere.
Every refusal answers in OLP's shape. Once the governing publication is known,
it also carries the RSL reference under assetrouter_rsl and a
Link: <...>; rel="license" header; a refusal reached before that — an
authentication failure, a malformed request — carries neither, because there
is no governing document to name yet.
A condition that is this server's own rather than the request's — no signing
key available to seal the evidence a Grant carries, this server's own ceiling
on concurrent origin retrievals — answers 503 temporarily_unavailable rather
than a 400. There is nothing in the request to correct, and a caller told
its request was malformed would stop instead of retrying; the same request may
be sent again unchanged.
Under the pointer profile the published document is the statement of terms,
so a scope with no current publication issues nothing: it offers no rows to
route against, and the resource is refused as one nothing licenses, for a free
region exactly as for a paid one. It is a state the tenant's operator clears by
publishing, not a request the caller can correct.
A licence that requires external fulfillment answers the conformant 400; a
client whose registration records the 402 licensing-required capability
receives that instead, led by the namespaced assetrouter_response_type
discriminator. The capability is registration metadata and cannot be
requested by a header or a payload.
POST /licensing/v1/tenants/{tenant_id}/olp/key
Authenticated and form-encoded, naming a resource. Present in every OLP
profile; this profile serves no encrypted resources, so it answers 403
insufficient_scope and never key material. A resource whose effective RSL
declares encrypted="true" is refused at /token, at profile validation,
before any Grant or token exists.
POST /licensing/v1/tenants/{tenant_id}/olp/introspect
RSL Open Licensing Protocol, form-encoded, HTTP Basic, authenticated as a
Resource Server client the tenant authorized for the origin it serves:
token=<the License Token as presented>&
resource=https://atlas.example/reports/q1
Both fields are required. This is the authoritative online check: a token's
own dates say nothing about whether the Grant behind it is still good, and
this is the only surface that can answer that.
Always 200 when the request itself was well formed, including for a token
that is no good — RFC 7662 semantics. A 401 here would leave a Resource
Server unable to tell "your token is no good" from "your own credential is no
good", which are its client's problem and its own respectively.
An unusable token — unknown, expired, revoked, presented by a client that
is not its audience, or whose Grant has been revoked, suspended or has expired
— answers exactly:
{"active": false}
and nothing else. Every one of those answers is identical, deliberately: told
apart, this endpoint would report which Grants and tokens exist in a tenant to
anybody holding one valid Resource Server credential. The reason is recorded
against the tenant's own introspection log, which is where its operator reads
it.
An active token answers seven fields, six of them RSL 5.5.3's own:
active, token_type, jti (the License Token's public id), the exact
resource as submitted, permitted, exp, and license — the granted
<license> element itself, as the Recommendation's own examples show it, on
every active answer whether or not it permits.
license is what was granted, not what the renderer would produce today. It
is frozen on the token when the token is minted and read back with it, so a
publication that is later superseded, or a document later withdrawn, does not
change what an answer says was granted. The element carries its own namespace
declaration, so a Resource Server can hand it to an RSL parser as it stands.
A seventh field is ours and namespaced: assetrouter_grant_id, which the
Access Events of a later release need. A Resource Server does not read it —
it reads active and permitted, and nothing else selects a row — and it is
absent on a governed free answer, which has no Grant to name. Read it as optional, as a Resource Server reads every
field it does not select on.
permitted is a second question and not a restatement of active. A licence
granting rights without content access answers active: true and
permitted: false; so does a token asked about a resource its Grant does not
cover. A not-permitted answer also carries reason — RSL 5.5.3's own field,
carrying a stable machine string rather than the prose the OLP draft shows, so
it can be logged and searched. Four reasons reach the wire:
resource_not_covered, access_not_licensed, access_allowance_exhausted
and usage_not_covered. Every other reason this
server records answers {"active": false} and is disclosed to nobody.
What a token reaches is a region, and the region is frozen on the token
when it is minted. For a token issued under a Grant over the whole scope it is
the path pattern of the row that governed the request, minus the pattern of
every row of that publication strictly narrower than it. For one issued under
a Grant over a single resource it is that resource's own path, anchored, with
no exclusions — the Grant bought one resource and its token reaches that
resource alone.
Those narrower rows are the sub-regions the Publisher said something different
about, so they carry their own terms and their own price and an agent that
wants one asks for its own token. Within the region the token is authority for
every resource, so an agent acquires one token per region rather than one per
URL. The host is asked separately and against live bindings, because a retired
host has stopped being the scope's. A resource the region does not reach — a
different host, a path outside the pattern, a path the region excludes —
answers resource_not_covered, one answer for all of them, so that a Resource
Server credential cannot map a Publisher's row geometry by asking about paths.
A resource outside an exact-resource token's one-path region answers the same
way, for the same reason.
What is re-read on every call, beyond the token's own dates and the Grant:
the client the token was issued to, this tenant's authorization of that
client, and the binding saying that client may act for that beneficiary. Each
of the three is a separate lever of a different width — a suspended
registration stops that client in every tenant, a revoked authorization stops
it with this Publisher alone, and suspending a beneficiary stops the clients
acting for it — and all three now reach tokens already issued rather than only
the next one requested.
GET /licensing/{environment}/.well-known/jwks.json
Public. Every key an environment's certificates may be signed under,
including retired ones — a certificate signed three years ago must still
verify.
GET /licensing/{environment}/certificates/{certificate_id}/status
Public, never cached. Four fields: certificate_id, grant_state,
state_effective_at, observed_at. Unknown, malformed and other-environment
certificate ids answer identically.
GET /licensing/{environment}/rsl/scopes/{scope_id}
Public, never cached. The RSL document governing this License Scope now,
served as application/rsl+xml. This is the address a Publisher pastes into
its origin once, as a robots.txt License: line or an HTTP Link header:
it is keyed on the scope, so a publication superseded next quarter does not
turn that line into a dead link.
A withdrawn document answers 410 document_withdrawn rather than 404,
because a crawler told "not found" reads a misconfiguration while "gone" is
what actually happened. Unknown, malformed and other-environment scope ids
answer 404 unknown_document, identically. An observation of a document
somebody else's origin served is never answered here, whatever scope it was
recorded against.
GET /licensing/{environment}/rsl/{document_id}
Public, never cached. The document one publication froze, at an address that
never moves, served as application/rsl+xml. It stays readable after that
publication is superseded or withdrawn, because a Grant issued under it
outlives it and this is where its terms were stated.
GET /licensing/{environment}/terms/{terms_id}
Public, never cached. The governing prose a licence document's rows point at
and a certificate names, served under the media type it was recorded with and
always as an attachment, with X-Content-Type-Options: nosniff. A verifier
downloads these bytes and recomputes the terms digest a certificate carries.
Nothing supersedes or withdraws a terms snapshot: a row stops pointing at one,
and the bytes stay where the evidence citing them said they were. Unknown,
malformed and other-environment terms ids answer 404 unknown_document,
identically, and the route shares the document routes' rate ceiling.
All three routes carry two codes of their own, unknown_document and
document_withdrawn, which are not in the registry below and are not part of
this contract — like unknown_environment on the two routes above. Their
callers are crawlers and licence readers who have never read this document,
and folding their codes into a contract written for integrations would make an
RSL client's error handling depend on it.
The public endpoints have per-address request limits. The two RSL document routes share a limit of 120 requests per minute. Certificate status and JWKS each have a separate limit of 60 requests per minute. Cache the JWKS according to its Cache-Control response header. Limits can vary by service configuration.
Two more buckets sit beside them, on the surfaces where a credential is
collected rather than a document read: the agent-registration claim and the
connect page. Both are tighter — a shared ceiling of 20 a minute
(ASSETROUTER_LICENSING_REGISTRATION_RATE_LIMIT) — because they are the only
unauthenticated routes here that guesses can be thrown at, and low enough that
guessing is hopeless while an operator onboarding a batch of buyers in one
afternoon never meets it.
Exhausting one answers 429 rate_limited, uncacheable. That code belongs to
these six routes alone, like unknown_document beside it — no route under
this contract answers it any more.
These ceilings are per process unless the deployment's cache is shared.
They are counted in CACHES["default"], so under a per-process backend each
worker keeps its own tally and the effective ceiling is the configured number
multiplied by the number of workers. The deployment warns about this at
startup rather than leaving it to be discovered from traffic.
GET /licensing/v1/tenants/{tenant_id}/measurements
How much this server observed, for this tenant. Four optional query
parameters, all narrowing:
| Parameter | Meaning |
|---|---|
customer |
A beneficiary's public id. Narrows the tiers to that customer. An agent may name only a beneficiary it is bound to act for, and must name one when it acts for several. |
scope |
A License Scope's public id. Narrows the tiers to that scope. |
since, until |
ISO 8601 instants with an offset. |
{
"measurements": [
{"tier": "observed", "accesses": 1420},
{"tier": "confirmed", "accesses": 0}
],
"introspections": 1533,
"note": "An observed figure counts what this server authorized..."
}
measurements carries every tier, always, each under its own label. A
figure never travels without saying which tier it is: observed is what this
server saw itself authorize, confirmed is what a Resource Server attested
after serving. Confirmed is zero until that surface exists, and it is zero
because its own source is empty rather than because observed was copied into
it.
introspections carries no tier, deliberately. It counts decisions
including refusals — the reliability question a Publisher asks when an
integration starts failing — and is not a measurement of access. It is keyed
on the client that asked, unlike the tiers above, which are keyed on the
customer the access was licensed for.
The observed figure is a floor. A Resource Server may cache introspection
answers within RFC 7662's bounds and serves those accesses without asking
again, so the real number is at least this one and may be higher. The response
says so in its own note. Billing rests on the licensee's declaration under
the governing terms; this is corroboration.
Refusals: scope_not_in_tenant for a scope that is not this tenant's,
unknown_principal for a customer that is not, and invalid_client for an
agent naming a beneficiary it may not read or failing to name one when it acts
for several.
Error registry
Every code this surface can answer with. A code's meaning is stable; the list
grows as endpoints ship.
| Code | HTTP | Description |
|---|---|---|
free_and_paid_for_one_use |
422 | This row offers the same use both free and for payment. Free admission would always win that pair, so the paid offer could never be sold. Offers may permit different uses at different prices; the same use may not be free under one and paid under another. |
free_policy_offer_not_free |
422 | A Grant issued because the document said the use is free must rest on a free offer. Resting one on a priced offer would hand out for nothing what a Publisher put a price on. |
idempotency_key_required |
400 | Every mutation must carry an Idempotency-Key header. |
invalid_request |
400 | A field is missing, of the wrong type, or outside its allowed values. |
malformed_json |
400 | The request body is not valid JSON. |
invalid_client |
401 | The request did not authenticate a client authorized in this tenant. |
grant_issuer_binding_required |
403 | Minting or ending an authorization is delegated authority, and this client holds no Grant issuer binding in this tenant. |
no_asserted_host |
422 | This License Scope has asserted no host, so a published document would name no origin and rest on no asserted control. |
no_covering_row |
422 | The scope's current publication declares no rights over that path, so nothing licenses it. |
no_current_publication |
422 | This License Scope has published nothing, so there is no licence to issue against: a scope licenses only against its current publication. |
access_basis_not_recognized |
422 | That is not an access basis this server knows. Only 'single_fetch' can run out, so a value it does not recognise would be read as one that never does, and a row meant to license one copy would hand out many. |
no_offers |
422 | A rights row must carry at least one offer. A row with none licenses nothing and would publish a <content> with no <license> inside it. |
no_permitted_usage |
422 | A rights row must permit at least one usage class. A row that permits nothing licenses nothing, and publishing it would render a licence whose permissions are empty. |
no_rows |
422 | This License Scope declares no rights, so there is nothing to publish: a document with no content element licenses nothing. |
nothing_to_withdraw |
422 | Every publication of this License Scope is already withdrawn; a withdrawal is one-way and is never made twice. |
offer_ambiguous |
422 | This row sells the requested use more than one way, and the request does not say which. Send back the <license> element of the offer you are asking under — the published document carries one per offer — so the answer is the price you chose rather than one we picked. |
offer_not_a_mapping |
422 | An offer is a mapping of what it permits, what it costs, how far it reaches and what access it brings. |
offer_not_in_publication |
422 | The published document carries no such offer. Either the row is not in it, or the row carries no offer of that payment type and reach. A Grant is issued under an offer the document actually published, because that document is what a buyer read and what a dispute reads back. |
offer_reach_mismatch |
422 | That offer does not reach what this Grant would cover. An offer sold for one resource cannot become a subscription to the section, and a subscription cannot be narrowed to one article by naming it: the reach is what the Grant encloses, so it is the Publisher's to set and not the request's to reinterpret. |
offer_repeats_type_and_reach |
422 | Two offers on this row share a payment type and a reach, and an offer is named by that pair. RSL carries no duration, so a monthly and an annual subscription over one section are indistinguishable in the published document and nothing downstream could tell which one a Grant was issued under. A Publisher selling both publishes a row for each, over its own path. |
offers_not_a_list |
422 | A rights row's offers are a list. A row sells its content one or more ways, and the list is what carries the order they are published in. |
pattern_anchor_not_final |
422 | '$' anchors the end of a path and may appear only as the last character of a pattern. |
pattern_has_whitespace |
422 | A path pattern carries no whitespace. Two patterns differing only in spacing would be one rule to a reader and two rows to the database. |
pattern_has_wildcard |
422 | A path pattern is a prefix with an optional trailing '$'; it carries no '*'. Which of two rows is the more specific has to be readable from the rows themselves, and a wildcard in the middle makes that answerable only by testing a concrete path against both. |
pattern_is_not_the_rows |
422 | A rights row is identified by its path pattern, so an update at one row's address may not name a different pattern. Leave the pattern out, or write to the address of the row that pattern names. |
pattern_not_rooted |
422 | A path pattern starts at the site root, so it must begin with '/'. A relative pattern would mean something different depending on where a reader thought it started. |
pattern_not_text |
422 | A path pattern is text. A number or a null names no part of a site. |
pattern_overlaps_other_scope |
422 | A path pattern here overlaps or nests inside a pattern another License Scope of this tenant currently publishes on the same host. Two documents would claim the same paths, at two prices. |
permits_and_prohibits_conflict |
422 | A rights row permits and prohibits the same value. RSL 1.0 section 3.1.1 resolves that when reading somebody else's document, but a document we author is ours to get right before it is served. |
publication_already_superseded |
422 | That publication has already been replaced by a later one. |
publication_already_withdrawn |
422 | That publication is already withdrawn, and a withdrawal is one-way. |
publication_frozen |
422 | A publication records what was served and is never rewritten or removed; withdraw it instead. |
publication_not_current |
422 | That decision was made under a publication this License Scope has since replaced, so binding it would attach an external workflow to terms the Publisher no longer offers. |
publication_not_in_scope |
422 | That publication belongs to another License Scope, so its rows are not this scope's to restore. |
publication_withdrawn |
422 | The publication this request rests on has been withdrawn. The Publisher has taken back the statement it made, and a withdrawal is one-way: publishing again is what reopens licensing. |
resource_not_enclosable |
422 | That resource cannot be enclosed by a token. A token cut from an exact-resource Grant takes the resource's path as its whole region, and RFC 9309 reads * in a path as a wildcard — so a Grant over a resource containing one would reach every path that wildcard spans rather than the single resource bought. Percent-encode it to name the literal character. |
role_not_permitted |
403 | This client's role in this tenant does not permit that action. |
row_repermits_a_prohibition |
422 | A row covering these paths prohibits a value this row permits. The published document states each row on its own, so the narrower one would offer a use the evaluator refuses — every row covering a path is combined and any prohibition wins. A reader would be promised a licence this server will not honour. Narrow the permission, or lift the prohibition on the wider row. |
rows_already_restored |
422 | This scope's rights rows already say exactly what that publication said, so there is nothing to restore. |
tenant_inactive |
403 | This tenant is suspended or terminated and accepts no commands. |
terms_media_type_not_supported |
422 | Governing terms are recorded as a PDF, plain text or Markdown. This server serves those bytes back at a public address, and a type a browser renders as a page would run the document's own markup there. |
terms_not_in_tenant |
422 | That Terms Snapshot belongs to another tenant. A row may only reference governing prose its own Publisher recorded. |
unknown_action |
403 | That action is not defined. |
unknown_certificate |
404 | No such certificate in this tenant. |
unknown_connect_session |
404 | No such connect session in this tenant. |
unknown_content_license |
404 | No such rights row in this tenant. |
unknown_grant |
404 | No such grant in this tenant. |
unknown_license_decision |
404 | No such license decision in this tenant. |
unknown_license_scope |
404 | No such license scope in this tenant. |
ambiguous_license_scope |
422 | One scope decides a Grant's terms, so a call naming several cannot say which licence governs. Name one scope per call. |
unknown_offer_reach |
422 | That is not a reach this server can enforce. An offer reaches either the region its row covers or only the exact resource it was bought for, because those are the two a Grant can be written to enclose. |
unknown_principal |
404 | No such principal in this tenant. |
unknown_publication |
404 | No such publication in this tenant. |
unknown_terms |
404 | No such terms snapshot in this tenant. |
method_not_allowed |
405 | That method is not defined for this route. |
alias_resolution_conflict |
409 | This resolution conflicted with a concurrent write and was not completed. Retry it. |
decision_already_bound |
409 | This decision is bound to a different external source or window. |
decision_not_cancellable |
409 | This decision is already fulfilled or cancelled. |
decision_not_open |
409 | This decision is no longer open. |
idempotency_conflict |
409 | This idempotency key was already used for a different request. Use a new key for a new request. |
object_state_forbids |
409 | The target's current state does not admit that action. |
renewal_already_issued |
409 | This entitlement already produced a successor for this predecessor. |
request_too_large |
413 | The request body is larger than this API accepts. |
unsupported_media_type |
415 | A request body must be sent as application/json. |
access_basis_not_selectable |
422 | That is not a selectable access basis. A basis nothing can enforce would be a promise with no mechanism behind it. |
enrolment_already_claimed |
422 | That registration has been claimed; it has no code to re-issue. |
host_not_a_host |
422 | That is not a hostname: it must be lowercase, with no scheme, no port, no path and no trailing dot. |
asserting_party_required |
422 | A snapshot must say who is vouching for it; a certificate prints that too. |
agent_not_active |
409 | That agent's registration is suspended or revoked. Its operator resolves that with us; it is not the Publisher's to fix. |
agent_not_registered |
404 | No agent is registered under that hostname. The customer's operator has not registered it yet, or it was typed wrong. |
assurance_needs_self_assertion |
422 | Only the agent itself may assert this level: it records that the party demonstrated control of its own domain, so no other issuer may claim it on that party's behalf. |
assurance_not_issuable |
422 | This scope may issue only publisher_asserted and domain_verified: every other level needs evidence Agent Trust gathers. |
scope_not_in_tenant |
422 | A named license scope does not belong to this tenant. |
client_not_active |
422 | That client's registration is suspended or revoked, so this tenant cannot authorize it. |
client_not_authorized |
422 | This tenant does not currently authorize the client the request names. |
client_not_bound_to_principal |
422 | This client is not bound to act for that beneficiary in this tenant. |
customer_reference_required |
422 | A connect session must name the customer it is for, in the Publisher's own terms — that reference is what the beneficiary is keyed on. |
data_processing_agreement_missing |
403 | This tenant's organization has not accepted the data processing agreement. Customer data may not reach us before it does. |
data_processing_agreement_stale |
403 | This tenant's organization accepted an older data processing agreement and must accept the current one. |
country_not_alpha_2 |
422 | A country is an ISO 3166-1 alpha-2 code, the same form the RSL geo vocabulary insists on. |
decision_expired |
422 | This decision's window has closed. |
decision_frozen |
422 | A recorded decision is immutable. |
decision_not_fulfillable |
422 | Only a decision awaiting external fulfillment can be bound. |
decision_state_not_storable |
422 | That is not a storable decision state. |
document_not_yet_published |
422 | This License Scope's licence document is not published yet, so its terms are readable nowhere outside this server. Publishing is what opens licensing against it. |
entitlement_evidence_not_serializable |
422 | The entitlement is not a JSON document this server can digest. |
entitlement_period_empty |
422 | The asserted period never opens. |
entitlement_period_ended |
422 | The asserted period is already over. |
entitlement_period_required |
422 | The entitlement asserts no period. |
entitlement_period_too_long |
422 | The asserted period is longer than this scope will issue against. |
encrypted_resource_not_supported |
422 | This License Server profile serves no encrypted resource. An EMS-enabled profile is required before one can be licensed. |
entitlement_period_unparsable |
422 | The asserted period is not a readable ISO 8601 instant. |
license_server_not_named |
422 | This policy publishes its licence without naming a License Server, so its terms are taken from the document itself and no token is issued against it. |
external_subject_required |
422 | A resolution must name the external subject it maps; the alias tuple is what makes the resolution idempotent. |
fulfillment_window_empty |
422 | The fulfillment window never opens. |
fulfillment_window_too_long |
422 | The fulfillment window is longer than this server will hold a decision. |
grant_frozen |
422 | A Grant's issued terms are immutable. |
grant_issuer_licensor_mismatch |
422 | This binding delegates another Licensor's rights. |
grant_issuer_not_permitted |
422 | This client's Grant issuer binding is not in force. |
grant_issuer_scope_not_covered |
422 | This binding does not cover the License Scope of the request. |
grant_issuer_source_not_covered |
422 | This binding does not delegate that Grant source. |
grant_state_not_storable |
422 | A Grant is never stored as expired. Expiry is a fact about the clock and is derived from the window, so no sweep has to run for a Grant to stop being usable. |
issuer_source_types_required |
422 | A Grant issuer binding names the sources it may assert. One that names none delegates nothing and would refuse every request it received. |
label_required |
422 | An enrolment must name the agent, so an operator can tell one of a customer's agents from another later. |
legal_name_required |
422 | A beneficiary must be named; a certificate prints it. |
licensor_retired |
422 | The Licensor behind this scope has left service. |
malformed_resource |
422 | The submitted resource cannot be read under Resource Equivalence Profile v1. |
no_approved_origin |
422 | This License Scope has no verified origin binding, so the document would name no host and rest on no proven control. |
no_snapshot_change |
422 | A snapshot update that changes nothing would only re-date an assertion nobody made again. |
document_not_well_formed |
422 | The rendered licence document could not be read back through this server's own RSL parser -- either it does not parse, or the parser could not account for part of it. A declared value most likely carries a character XML cannot represent. The document is not published, and no publication observation is recorded. |
organization_not_cleared |
403 | The platform has not cleared this organization to hold a tenant in that environment. A live tenant waits on an approved publisher application; a sandbox tenant also accepts one still under review. |
origin_exclusions_not_renderable |
422 | An approved origin binding excludes paths inside its own prefix, and the published document has no way to carry that narrowing: it would claim terms over content this server refuses to license. |
payment_member_not_renderable |
422 | The payment declaration carries a member RSL 1.0 section 3.7 has nowhere to write, and publishing it would drop that member in silence. |
payment_value_not_a_string |
422 | A payment declaration is published exactly as it was recorded, so its values must already be strings: rendering a number would publish our spelling of the amount rather than the Publisher's. |
possession_declaration_not_applicable |
422 | A possession declaration says the buyer already holds the content it is licensing rights over, which only means something when no access is granted. Record 3.10 admits the claim for license_only alone. |
possession_digest_malformed |
422 | A possession declaration must be a SHA-256 digest in lowercase hex. |
predecessor_source_not_renewable |
422 | Only a publisher-entitlement Grant is renewed; every other source is asserted afresh. |
principal_not_active |
422 | This beneficiary is suspended. |
reporting_member_not_renderable |
422 | The reporting obligation carries a member RSL 1.0 section 3.12 has nowhere to write. |
reporting_profile_required |
422 | A reporting obligation without a profile URI names nothing a client could comply with. |
reporting_type_unknown |
422 | That reporting type is outside the closed reporting vocabulary. |
reporting_value_not_a_string |
422 | A reporting obligation is published exactly as it was recorded, so its values must already be strings. |
request_refused |
422 | The command refused this request and the refusal is not part of the versioned error contract. |
resource_not_comparable |
422 | Resource Equivalence Profile v1 cannot read this resource, so a Grant frozen on it could never be matched against a later request. |
resource_outside_license_scope |
422 | No verified origin binding of that License Scope covers this resource, so the scope never claimed it. |
scope_not_active |
422 | This License Scope is not active. |
scope_outside_licensor |
422 | A listed scope belongs to a different Licensor than the binding names. |
source_ref_derived |
422 | A free-policy Grant derives its source reference from the publication it was issued under; supplying one would let two answers disagree. |
source_reference_not_namespaced |
422 | An external source reference must be namespaced by its issuing system. |
source_reference_required |
422 | A binding must name the external source that will fulfil it. |
source_type_not_issuable |
422 | This scope issues only publisher_entitlement and free_policy Grants; the other sources activate in a later release. |
subject_resource_not_allowed |
422 | A scope-subject Grant freezes no resource: leaving one here would make the subject depend on which field a reader looked at first. |
subject_resource_required |
422 | An exact-resource Grant must freeze the resource it covers. |
submitted_license_not_conformant |
422 | The submitted licence is not conformant and cannot be compared. |
unknown_alias_issuer |
422 | That is not an alias issuer. |
unknown_assurance_level |
422 | That is not an assurance level. |
unknown_payment_type |
422 | That payment type is outside the closed payment vocabulary. |
unknown_revocation_reason |
422 | That is not a revocation reason category. |
unknown_source_type |
422 | That is not a Grant source. |
usage_not_requestable |
422 | That is not a requestable usage. |
validity_window_empty |
422 | valid_until must come after valid_from. A window that never opens is an authorization that authorizes nothing. |
value_not_text |
422 | A field that must be text is not. |
value_too_long |
422 | A value is longer than the column that must hold it. |
version_not_approved |
422 | A version whose rights can still be edited has no document to publish: the bytes would stop matching the version that served them. |
signing_key_material_changed |
503 | The configured signing material no longer matches the published key. |
signing_key_not_configured |
503 | This environment has no signing key configured. |
signing_key_retired |
503 | The active signing key has been retired. |
signing_key_unavailable |
503 | No signing key is available to seal new evidence. |
Why a user or geo term refuses every request today
These reasons reach a caller on /token only, in the Open Licensing Protocol's
own shape — 400 with error set to invalid_license and the reason beside it
as assetrouter_reason — which is why they are not rows of the registry above:
user_claim_required,geo_claim_required— the terms condition this use
on a class of buyer or a jurisdiction, and no accepted claim states one.user_claim_invalid,geo_claim_invalid— a claim was present but its
issuer is not one this tenant accepts, or it is outside its validity window.user_class_not_permitted,jurisdiction_not_permitted— the claimed value
is not among those the terms permit.user_class_prohibited,jurisdiction_prohibited— the terms prohibit the
claimed value outright.
user_claim_required and geo_claim_required are not missing request
parameters. There is no field a caller can send to satisfy them, and adding one
would defeat what they are for.
A <permits type="usage"> term grants. A <permits type="user"> or
<permits type="geo"> term is a condition on that grant — a statement about
who the buyer is, or where the processing happens. This server does not take the
buyer's word for either: it answers them from a Claim, an issuer-accepted
classification carrying issuer, provenance, assurance level, observation time
and validity. A header is not a claim, and the request's IP address is never a
jurisdiction, and this server refuses that provenance by name.
User and geographic conditions require verified claims from an issuer accepted by the publisher. Without a matching claim, the request is refused. There is no public claim-submission interface. Publish a user or geographic condition only when a compatible claim source has been configured; otherwise it will prevent access.