Contrat

L'API de licence, v1

Chaque route, champ et code de refus de la surface de gestion, avec les conventions communes : authentification, idempotence, tenancy et modèle d'erreur.

Parcourir la documentation

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.