Contract

Authorizing a customer's agent

One call from your commerce system when an order completes, and two for the life of the subscription: how a customer who already pays gets their agent licensed.

Browse documentation

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, like agent.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:

  1. A client credential for your system, authorized in your tenant as a
    tenant operator. It is what your commerce authenticates with.
  2. 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 with grant_issuer_binding_required. An AssetRouter operator
    creates it — it is not self-service, because minting is a delegated
    authority.
  3. 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 like ag_live_ or ag_sb_
    followed by twenty characters.
  • A customer with no id yet leaves it empty. We create the registration and
    return a claim_code you 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 minted agent id 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 named client_id is 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 user https
    (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 supply agent, 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 at publisher_asserted assurance and
    can never reach domain_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 with host_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_scope rather 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:

  1. The beneficiary your customer reference names, created on first sight.
  2. The authorization admitting that agent into your tenant, with the scopes
    you named.
  3. The binding saying the agent acts for that beneficiary.
  4. The Grant — publisher_entitlement, valid until period_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:

  1. Authorize a registered agent. The answer carries a grant and a
    valid_until equal to the period_end you sent, to the second.
  2. Send the same call again with the same key. "replayed": true, same ids,
    nothing new created.
  3. Send it again with a different period_end under the same key.
    409 idempotency_conflict.
  4. Authorize a hostname nobody registered. 404 agent_not_registered, and
    nothing was written — the beneficiary does not exist afterwards either.
  5. Renew, then revoke the successor. Ask the agent's operator to call
    /token in between: it succeeds after the renewal and is refused after the
    revocation.
  6. 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.