Who this is for. Whoever owns a Publisher's commerce or subscription system
and has to make it license an AI agent for a customer who already pays. It is
written to be implementable without reading our code: one field on your order
form, one call when the order completes, and two calls for the life of the
subscription.
licensing-v1 owns the conventions this page uses and does not
restate: authentication, idempotency, the error model, tenancy.
Part 1 — For whoever is deciding
What you are building
Your customer already pays you. Their AI agent wants to read the content that
subscription covers, and the agent will present a credential of its own to a
gate in front of your content. Your job is to tell us, once, that this agent
acts for that customer under that subscription — and to tell us again when the
subscription renews or ends.
That is the whole integration. The agent gets its own credential from us
directly, so no secret of theirs ever passes through you, and the gate is a
separate document (resource-server-contract). What this page
covers is the link between your customer record and their agent.
What it costs you
- One field on your order form, where the customer types or confirms the
agent's name. It is a hostname, likeagent.northwind.example, and nothing
else — no URL, no key, no upload. - One call from the code that already marks the order paid. It is
idempotent, so retrying it on a timeout is safe. - One call on renewal and one on cancellation, from wherever those already
happen in your system.
Nothing runs on your site: no JavaScript of ours, no redirect, no iframe. A
plain form field and three server-to-server calls.
What you must hold up
The customer reference is yours and stays yours. We key the beneficiary on
the identifier your own system uses for that customer — a CRM id, an account
number, a subscription id. Never an email address: it changes, it identifies a
person rather than an account, and it would be frozen into evidence. We return
that reference to nobody, and a GET on the beneficiary never echoes it.
The period you assert is what the licence lasts. The Grant expires exactly
when you say the subscription does. Assert the period you bill for, and renew
when you bill again. Nothing on our side extends a licence you did not extend.
The field is confirmed by the customer, not by us. Whatever you send us is
what your logged-in customer entered or confirmed on your page. If the name
arrives prefilled — an agent may hand its operator a link with its own hostname
in the query string — treat that as a convenience for the customer and nothing
more; the authority is the customer's confirmation and your call.
Part 2 — The contract
Before the first call
Three things exist at onboarding, and this page assumes all of them:
- A client credential for your system, authorized in your tenant as a
tenant operator. It is what your commerce authenticates with. - A Grant issuer binding on that client covering
publisher_entitlement
for your Licensor. It is what lets your call mint a Grant; a client without
one is refused withgrant_issuer_binding_required. An AssetRouter operator
creates it — it is not self-service, because minting is a delegated
authority. - A current data processing agreement. A tenant whose acceptance is missing
or stale cannot authorize anything, because this is the call through which
your customers' data starts reaching us.
The customer enters the agent
On the page where the customer manages the subscription — or on the order form
itself, for a plan that includes agent access — one text field:
Your AI agent's id (leave empty if you don't have one yet)
[ ag_live_7QK3M2XJ8VBN4RTY6WPZ ]
Where your agent runs [ agent.northwind.example ]
Two fields, and the first is optional — that is what lets a customer who has
never dealt with us complete checkout without stopping.
- A customer who already has an agent id pastes it. Their operator has it;
it is in the agent's own configuration. Ids look likeag_live_orag_sb_
followed by twenty characters. - A customer with no id yet leaves it empty. We create the registration and
return aclaim_codeyou show them. The hostname their agent runs on is
welcome but optional: send it when the customer knows it, leave it out
when they do not. When you do send one, validate it before posting —
lowercase, no scheme, no port, no path — because a value that is present and
malformed is refused, while an absent one is not. A refusal on the form is a
typo fixed in place; a refusal after checkout is a support ticket.
Your integration branches once, on the answer: if a claim_code came back,
show it. Everything else is identical in both cases.
The call, when the transaction completes
POST /licensing/v1/tenants/{tenant_id}/agent-authorizations
Authorization: Basic base64(rsl_client_id:client_secret)
Idempotency-Key: <your order or event id>
Content-Type: application/json
{
"agent": "ag_live_7QK3M2XJ8VBN4RTY6WPZ",
"host": "agent.northwind.example",
"customer": {
"reference": "customer-1",
"legal_name": "Northwind Research"
},
"scopes": ["<license scope public id>"],
"entitlement": {
"reference": "sub-1001",
"period_end": "2026-12-31T00:00:00+00:00"
}
}
When one row sells more than one way. A rights row may carry several
offers — a section subscription beside a single-article purchase — and then
"which one is this agent authorized under" is a question this call cannot
answer for you: choosing would mean selling the cheaper offer in silence. Two
optional fields say it:
{
"payment_type": "subscription",
"pattern": "/reports/"
}
payment_type is RSL's own <payment type="…"> value for the offer being
sold — never an identifier, since an offer has no id of its own. pattern
names the row,
and is needed only when the Grant covers the whole scope; a call that names a
resource finds the row from the path.
Leave both out when the row sells one thing, which is the ordinary case and
the one this call was designed around. A call that names neither against a row
selling several ways is refused offer_not_in_publication rather than sold at
whichever price came first — so if you add a second price to a row and this
call starts refusing, that refusal is telling you to name the offer.
Field by field:
-
agent— optional. The customer's agent identifier, as we issued it:
ag_live_…in production,ag_sb_…in sandbox. Compared as a string.Leave it out for a customer whose agent we have never seen. We create the
registration and the answer carries both the mintedagentid and a
claim_code(see The answer). That is the normal
case for a new customer, and it means the buyer does not have to obtain an
identity from anyone before checking out.This is the RSL
client_id. If you also implement the OAuth Client ID
Metadata Document (CIMD), note that its identically namedclient_idis a URL
and cannot be used here at all — CIMD §4.1 forbids pairing it with
client_secret_basic, which this API requires. A URL in this field, or in the
Basic user-id above, is read by any conformant parser as the userhttps
(RFC 7617 §2), and fails in a way neither specification flags. -
host— optional. Where the customer's agent runs, as a bare
hostname. Ignored when you supplyagent, because a registration already
records its own.A hostname is not what makes an agent able to hold a licence — it is what an
agent can later prove, to be trusted for that domain. An agent that runs on
somebody else's infrastructure, or on no stable name at all, buys and fetches
exactly like any other; it simply stays atpublisher_assertedassurance and
can never reachdomain_verified. So send one when the customer knows it, and
leave it out rather than inventing one when they do not.Lowercase, no scheme, no port, no path. A value that is present and malformed
is refused withhost_not_a_host— the refusal an unvalidated form field
produces — while an absent one is accepted. -
customer.reference— your own identifier for the customer. It
identifies the beneficiary idempotently: the same reference always resolves
to the same beneficiary, whatever idempotency key the call carries. It is
read inside your tenant, so it has to be unique among your customers and
needs no prefix to keep it apart from another Publisher's. -
customer.legal_name— the name that goes on the licence evidence. Your
customer's business name, as your contract has it. -
scopes— the licence scope the subscription is sold under, handed to
you at onboarding. It is a list, and exactly one entry is accepted: one
scope decides the Grant's terms, so a call naming two is refused with
ambiguous_license_scoperather than issued under whichever came first. -
entitlement.reference— your identifier for the subscription or order,
frozen as evidence. -
entitlement.period_end— when it ends, as an ISO 8601 timestamp with an
offset. Required: a call with no readable period is refused rather than
issued open-ended, because a perpetual licence granted by an integration bug
looks exactly like a correct one.
Use your own order or event id as the Idempotency-Key. Replaying the key with
the same body returns the first outcome with "replayed": true; replaying it
with a different body answers 409 idempotency_conflict. So a webhook that
fires twice is harmless, and a retry after a timeout is the right move.
What the call does, in one transaction
Four rows come into existence, or are found already there, and either all of
them land or none does:
- The beneficiary your customer reference names, created on first sight.
- The authorization admitting that agent into your tenant, with the scopes
you named. - The binding saying the agent acts for that beneficiary.
- The Grant —
publisher_entitlement, valid untilperiod_end, with your
entitlement reference and the governing terms frozen into its evidence — and
its certificate.
Second and later calls for the same customer and agent find rows 1 to 3 and
create only what is new. A customer running two agents makes two calls with two
hostnames: two authorizations, two bindings, one beneficiary, one Grant.
The answer
{
"operation": "agent_authorization.create",
"replayed": false,
"principal": "<beneficiary public id>",
"authorization": "<authorization public id>",
"grant": "<grant public id>",
"valid_until": "2026-12-31T00:00:00+00:00",
"agent": "<the agent id, whether you supplied it or we minted it>",
"claim_code": "<present only when we created the registration>"
}
claim_code appears only when you omitted agent. When it is there, show
it to your customer with the agent id beside it: their agent presents the two
together, once, to collect its own credential. We never send you the secret, and
the code is single-use and expires — a lost one is re-issued, never recovered.
When you supplied agent, the field is absent and nothing about your flow
changes.
Keep the grant id against the subscription. It is what you renew and what you
revoke.
Refusals
The error model is licensing-v1's; these are the codes this call
adds and the ones from elsewhere you will meet.
| Code | Status | What it means for you |
|---|---|---|
agent_not_registered |
404 |
No agent is registered under that hostname. The customer's operator has not registered it yet, or typed it wrong. |
agent_not_active |
409 |
The agent exists and is suspended or revoked. Not yours to fix; the customer's operator resolves it with us. |
scope_not_in_tenant |
422 |
A scope you named is not one of yours. A configuration error on your side. |
entitlement_period_required |
422 |
period_end is missing or unreadable. |
data_processing_agreement_stale |
403 |
Your tenant's acceptance is missing or out of date. An onboarding matter, not a code path. |
grant_issuer_binding_required |
403 |
Your client holds no issuer binding covering publisher_entitlement. An onboarding matter. |
idempotency_conflict |
409 |
Same key, different body. |
Nothing here is retried into success: every refusal is either a fact about the
agent, a fact about your configuration, or a key reuse. Surface
agent_not_registered to the customer in words — we could not find an agent
by that name; check the hostname with whoever runs it — and log the rest.
Renewal
When you bill the next period, reassert it against the Grant you kept:
POST /licensing/v1/tenants/{tenant_id}/grants/{grant_id}/renewal
Idempotency-Key: <your renewal event id>
{"entitlement": {"reference": "sub-1002", "period_end": "2027-12-31T00:00:00+00:00"}}
This creates a successor Grant with its own window and its own certificate,
linked to the one it follows, and never touches the predecessor. Keep the new
grant id. Non-renewal needs no call: the Grant expires at its period_end
and the agent's tokens stop at the next check. (Shipped; see
licensing-v1.)
Cancellation and refunds
When a subscription ends before its period does:
POST /licensing/v1/tenants/{tenant_id}/grants/{grant_id}/revocation
Idempotency-Key: <your cancellation event id>
{"reason_category": "subscription_ended"}
The effective time is now and cannot be supplied. The agent's tokens stop
permitting at the next check. The certificate stays as evidence that the right
was issued; the public status route is what says it has ended. (Shipped.)
Blocking a customer — as opposed to ending one subscription — is a different
act: suspending the beneficiary, which stops every agent bound to it at once and
leaves the agents' authorizations in place for when the block lifts. It is a
back-office operation, not a commerce one, and outside this page.
Two agents, one customer
Nothing special. Two calls with two hostnames and the same customer reference
produce two authorizations and two bindings on one beneficiary and one Grant.
Revoking the Grant stops both. Withdrawing one agent is an authorization
revocation, which is a back-office act rather than a commerce one.
Part 3 — Verifying you got it right
Against your sandbox tenant, before the first real customer:
- Authorize a registered agent. The answer carries a
grantand a
valid_untilequal to theperiod_endyou sent, to the second. - Send the same call again with the same key.
"replayed": true, same ids,
nothing new created. - Send it again with a different
period_endunder the same key.
409 idempotency_conflict. - Authorize a hostname nobody registered.
404 agent_not_registered, and
nothing was written — the beneficiary does not exist afterwards either. - Renew, then revoke the successor. Ask the agent's operator to call
/tokenin between: it succeeds after the renewal and is refused after the
revocation. - Check what the customer sees. Enter a URL instead of a hostname in your
form; it should be refused on the form, before any call.
Tell us what you observe. You are the first commerce system to integrate against
this call, and a divergence you find is one nobody else has hit yet.
Reference
- licensing-v1 — authentication, idempotency, the error model,
and the renewal and revocation routes this page reuses. - resource-server-contract — the gate in front of your content,
which is where the agent's token is checked.