---
title: Callers and tokens
description: Caller kinds, how each is recognized, customer authority, and scope translation.
sidebar:
  order: 3
---

Callers are classified, admitted and guarded by the oRPC middleware chain (`authenticate`, `admitCaller`, `withActor`,
`guard`; see [the API pipeline](/architecture/api-pipeline)). Each procedure declares its callers and scopes once, in
its `api` metadata in `apps/api/contracts`.

The API never trusts a token on its signature alone. It classifies every token into a **caller** using
`client_id`, `sub` and `act`, checks those against configured client IDs, and then resolves a live **actor**, with
lifecycle and delegation checks, before guards, input validation or the handler run. Holding a
user `sub` alone never makes a caller a human customer.

## Consumers and credentials

| Consumer                       | Credential presented to the API                                                    |
| ------------------------------ | ---------------------------------------------------------------------------------- |
| Webshop, guest visitor         | Storefront M2M token (client credentials) plus a guest session header              |
| Webshop, signed in             | The customer's own access token for the API audience, nothing else                 |
| MCP server                     | An On-Behalf-Of token exchanged from the MCP client's token                        |
| Backoffice                     | Backoffice M2M token plus an operator attribution header                           |
| Webshop, invited-agent session | Dedicated web-agent-forwarder M2M token plus a bearer-bound invited-agent context  |
| Webshop, Poppy agent browser   | Dedicated web-agent-forwarder M2M token plus the agent's original browser cookie   |
| MCP server, signed-out Poppy   | Dedicated MCP-agent-forwarder M2M token plus the original token as `Poppy-Subject` |
| Poppy personal agent           | Calls the API and MCP directly, with a credential issued through the Auth0 proxy   |

## Caller kinds

| Caller kind     | How the API recognizes it                                                                                                                                 | Credential source                                      | Maps to                                                                                                                 |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `customer`      | User `sub`, no `act`, `client_id` is the website client                                                                                                   | Website login with the API audience (refresh via MRRT) | `customer` actor (channel `web`), owner is `sub`; account lifecycle state still applies                                 |
| `delegated`     | User `sub`, outermost `act.sub` equals `client_id` equals the MCP Custom API client                                                                       | MCP server On-Behalf-Of exchange                       | `agent` actor with `origin: 'oauth'` (channel `mcp`), account cart, no human-only operations                            |
| `guest`         | `sub` is `<client_id>@clients`, `client_id` is the storefront client, a `Guest-Session` header                                                            | Client credentials from the webshop                    | `guest` actor owning one browser cart                                                                                   |
| `invited-agent` | Web-agent-forwarder M2M token plus an invited-agent context                                                                                               | Webshop, for a delegated website session               | The existing delegated `agent` actor, with its original admission snapshot                                              |
| `operator`      | `sub` is `<client_id>@clients`, `client_id` is the backoffice API client, an `Operator` header                                                            | Client credentials from the backoffice                 | `OperatorActor`                                                                                                         |
| `poppy`         | An Auth0 access token for the API or MCP audience, minted through the Auth0 proxy's Custom Token Exchange profile and presented **directly** by the agent | Proxy Auth0 (Auth0 extension) to Auth0                 | Existing `agent` actor (`origin: 'poppy'`), with live session and grant checked through the Auth0 proxy's introspection |

Any other combination is rejected with 401 or 403, even when the signature is valid. That includes an unknown
`client_id`, an `act` chain whose outermost actor isn't the MCP client, or
an M2M token from a client with no caller kind. A user token from the website client that carries `act` is an
invited-agent login and is never accepted as `customer`.

Tokens use the `rfc9068_profile` dialect, so every access token carries `client_id` and `typ: at+jwt`.

## Customer authority

A customer is authorized by the access token alone. The API verifies the issuer, JWKS signature, audience,
`typ: at+jwt` and `exp`, classifies the caller from `client_id`, `sub` and `act` (website client, no `act`), checks the
operation's scopes, and scopes every row by ownership of `sub`. There is no `Shop-Session` header (a stale one is
ignored), no sign-in registration, no `sid`, generation or epoch in the API, and no `Web-Verification` context. The
`customer` actor is `{ kind: 'customer', channel: 'web', account: sub, cartOwner }`.

Account lifecycle state still applies. An account that is `deleting`, `deleted` or in `contact_transition` is refused with
401 `account_access_ended`, except for the email verification sync, which finishes a pending email change. Delegated,
invited-agent and Poppy actors keep their grant checks. The webshop owns the browser session: the Auth0 SDK cookie, the
revocation list and [back-channel logout](/architecture/request-flows/back-channel-logout).

Because the API is session-unaware, these properties follow:

| Property                  | Behaviour                                                                                   |
| ------------------------- | ------------------------------------------------------------------------------------------- |
| Revoking one web session  | Ends the web session; an issued API token stays usable until it expires                     |
| Copied-cookie protection  | Not at the API. The webshop validates its cookie against its revocation list                |
| Account-wide cutoff       | Not enforced by the API until token expiry. Deleted accounts fail where rows are gone       |
| Sign out everywhere       | The webshop revokes every website Auth0 session and its refresh tokens, plus a local cutoff |
| Sign-in activity          | Not recorded for customers                                                                  |
| Email change              | Other browser sessions stay; the account is refused while in `contact_transition`           |
| `WEB_SESSION_CONTEXT_KEY` | Signs only invited-agent contexts; it cannot forge customer authority                       |
| Poppy Sessions            | Checked live through Auth0 proxy introspection                                              |

The invited-agent context is a separate schema with its own verification branch, signed with
`WEB_SESSION_CONTEXT_KEY`, which is now used only for invited-agent sessions
(`invited-agent+jwt`). The storefront records an invited agent's delegated sign-in and ends it at sign-out itself, through
`@retail-demo/identity/delegation`, so there is no callback context. It is accepted only with a **web-agent-forwarder** M2M token, a client distinct from the guest storefront client so guest authority stays narrow.

## Guest sessions

The storefront mints guest sessions itself, from server-only code; the API only verifies them on cart routes. A session is
`gs1.<lowercase UUID>.<43-character base64url HMAC-SHA256>`. Guest reads without a session see an empty cart. Every
guest cart write requires it or returns 400 `guest_session_required`; a forged or malformed session, or one signed with
a previous key past its deadline, returns 401 `invalid_guest_session` with `WWW-Authenticate: Bearer error="invalid_token"`. The storefront also verifies
the cookie before it names a cart, so a forged or expired one reads and writes nothing there. `Shopper-IP` is
guest-only, at most 64 characters, and normalized only for rate limits. Both guest headers are refused on website
tokens.

New sessions always use `GUEST_SESSION_KEY`, bound on both the API and the storefront Worker. Rotation can configure
`GUEST_SESSION_KEY_PREVIOUS` together with `GUEST_SESSION_KEY_PREVIOUS_UNTIL` (ISO 8601); the previous key verifies only
before that deadline. To rotate, the owner adds a new Alchemy Random key, binds the old key for a 30-day window, and
removes it after expiry (see the API README).

The storefront M2M token identifies the **storefront**, not the visitor. The visitor's capability is possession of a
random cart id. The `Guest-Session` value is that id plus an HMAC computed with the shared guest key, which proves the id was minted
by the storefront. Two guests behind the same storefront token can't see each other's carts.

## Scope translation

Scope translation applies only to `delegated` (OAuth MCP) callers. Once the route guard has accepted a token's API
scopes for an operation, the API grants the actor that operation's domain scopes and nothing else. A `cart:write`
token therefore never becomes a general `poppy:write` actor.

`invited-agent` and `poppy` callers are never translated. Their actor keeps the immutable permissions it has today,
and their route guard requires that the operation's scopes fall within the mapped permissions **and** that the
service credential carrying them is allowed to forward agents. A read-only invited agent can't check out, whatever the agent-forwarder
token holds.

:::note[Human-only actions are blocked three times]
Auth0 grant scopes, the operation's declared caller kinds, and domain policy on the actor kind each refuse
human-only actions to agents.
:::

## Procedure caller declarations

`CallerKind` is `customer | delegated | guest | invited-agent | operator | poppy`. Every procedure
declares its admitted callers in `api({ callers, apiScopes, agentScopes })`, using the named lists in
`contracts/callers.ts`: storefront (`customer`, `delegated`, `guest`, `invited-agent`, `poppy`), account (storefront
without `guest`), checkout (`customer`, `invited-agent`, `poppy`) for the checkout reads and identity, agent checkout
(`delegated`, `invited-agent`, `poppy`) for saved-card checkout, operator, and a diagnostic list for `GET /v1/me`. `agentScopes` are the Poppy account scopes a Poppy caller needs; `apiScopes` are the Auth0 Shop
API scopes. The agent-facing procedures:

| operationId          | Route                                  | `agentScopes` | `apiScopes`            | callers        | tools       |
| -------------------- | -------------------------------------- | ------------- | ---------------------- | -------------- | ----------- |
| `searchProducts`     | `GET /v1/catalog/products`             | none          | `catalog:read`         | storefront     | mcp, webmcp |
| `getProduct`         | `GET /v1/catalog/products/{handle}`    | none          | `catalog:read`         | storefront     | mcp, webmcp |
| `listCollections`    | `GET /v1/catalog/collections`          | none          | `catalog:read`         | storefront     | mcp, webmcp |
| `getCart`            | `GET /v1/cart`                         | none          | `cart:read`            | storefront     | mcp, webmcp |
| `addCartItem`        | `POST /v1/cart/items`                  | none          | `cart:write`           | storefront     | mcp, webmcp |
| `updateCartItem`     | `PATCH /v1/cart/items/{variant_id}`    | none          | `cart:write`           | storefront     | mcp, webmcp |
| `removeCartItem`     | `DELETE /v1/cart/items/{variant_id}`   | none          | `cart:write`           | storefront     | mcp, webmcp |
| `checkoutSavedCard`  | `POST /v1/checkouts/saved-card`        | `checkout`    | `checkout`             | agent checkout | mcp         |
| `getProfile`         | `GET /v1/me/profile`                   | `poppy:read`  | `account:read`         | account        | mcp         |
| `listAddresses`      | `GET /v1/me/addresses`                 | `poppy:read`  | `account:read`         | account        | mcp         |
| `addAddress`         | `POST /v1/me/addresses`                | `poppy:write` | `account:write`        | account        | mcp         |
| `deleteAddress`      | `DELETE /v1/me/addresses/{id}`         | `poppy:write` | `account:write`        | account        | mcp         |
| `listPaymentMethods` | `GET /v1/me/payment-methods`           | `poppy:read`  | `payment_methods:read` | account        | mcp         |
| `listOrders`         | `GET /v1/me/orders`                    | `poppy:read`  | `orders:read`          | account        | mcp, webmcp |
| `getOrder`           | `GET /v1/me/orders/{id}`               | `poppy:read`  | `orders:read`          | account        | mcp, webmcp |
| `cancelOrder`        | `POST /v1/me/orders/{id}/cancellation` | `poppy:write` | `orders:write`         | account        | mcp         |
| `requestReturn`      | `POST /v1/me/orders/{id}/returns`      | `poppy:write` | `orders:write`         | account        | mcp         |

## Scope helpers and actor shape

`translateScopes` applies only to `delegated` OAuth callers. It returns a fresh copy of that operation's domain
`scopes` only when every required `apiScope` is granted, and returns undefined otherwise. It never grants more
than the one operation's scopes. `poppyPermits` instead checks immutable Poppy or invited-agent permissions through
this map; unknown permissions unlock nothing beyond the base scopes:

| Permission                       | API scopes                                            |
| -------------------------------- | ----------------------------------------------------- |
| Base, without account permission | `catalog:read`, `cart:read`, `cart:write`             |
| `poppy:read`                     | `account:read`, `payment_methods:read`, `orders:read` |
| `poppy:write`                    | `account:write`, `orders:write`                       |
| `checkout`                       | `checkout`                                            |

CallerKind `delegated` maps to `OAuthAgentActor`: `kind: 'agent'`, `origin: 'oauth'`, `channel: 'mcp' | 'openapi'`,
display name and domain, `oauth: { clientId, actorChain }`, `signedIn: true`, account subject, captured lifecycle
revision/epoch, translated domain scopes and the shared account cart owner. The actor chain is non-empty and its
outermost subject equals the token client ID. It has no Poppy session, opaque user ID or delegation snapshot.
Existing order/customer-activity attribution uses the innermost client the user consented to. Diagnostic observations
preserve the complete supported current-first actor chain, with intermediate/initiating identities and represented
account separately labelled; richer verified ingress facts are not replaced by a narrower domain projection. A target or
public actor claim is not verified authority. Structural chain coverage is explicit: complete, incomplete or invalid;
historical absence is not proof of completeness. Malformed/oversized snapshots are omitted whole, without upgrading
individual provenance or authorization. See [diagnostic investigations](/runbooks/audit-investigations).

CallerKind `invited-agent` maps to the existing `DelegatedAgentActor` with its immutable admission snapshot.
Existing Poppy and invited-agent actors may omit `origin`, which means Poppy.

The storefront runs the whole web-session surface itself: it merges the guest cart, serves the profile page and the
status receipts, and runs the account-security family (fresh verifications, security methods, embedded verification,
contact changes, contact methods and account deletion). None of them is a Shop API endpoint.

## Poppy and forwarded-agent admission

A `poppy` caller is either a single-level `personal_agent` CTE token from the shared Auth0 proxy client, or a separately
verified signed-out Auth0 proxy token. Dedicated `web_agent_forwarder` and `mcp_agent_forwarder` machine credentials are intermediate agent-forwarding
callers only: they require exactly one original artifact. `invited-agent` uses an independently verified bearer-bound
context and live durable delegation checks. Forwarded Poppy callers and direct Poppy tokens both use `/v1`: agent forwarders with
their verified web or MCP artifact, direct tokens with `Authorization: DPoP` and a proof. Live Poppy permissions
intersect with resource scopes and, for M2M agent forwarders, `agent:forward`. Empty-scope signed-out Auth0 proxy tokens have only the fixed
catalog and cart base authority. MCP verifies the original token and Session before OBO or the narrow signed-out agent forwarder.

Poppy requests act with the permissions the token carries that the Session still grants, the challenge scheme is chosen
before verification, and the DPoP replay record is shared by every isolate. See
[Poppy agents](/architecture/request-flows/poppy-agents#effective-authority) and
[Poppy compliance](/architecture/poppy-compliance).

## Poppy authority

| Property             | Rule                                                                                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheme by token kind | Session Tokens use `DPoP` and need a proof; they are never accepted as `Bearer`, in a URL or in a cookie. Everything else is a plain bearer                |
| Challenge scheme     | Chosen from the request before any verification, so a missing, malformed, expired or rejected token is answered in one scheme with `error="invalid_token"` |
| Effective scopes     | `token ∩ Session`: the Poppy permissions the token's API scopes cover, intersected with the Session's live scopes. Used by REST, MCP and the Company Agent |
| Stale tokens         | A signed-out token on a signed-in Session, or a signed-in token on a Session that signed out, acts signed out (`sign_in_required` for account actions)     |
| Proof replay         | An accepted `jti` is a row in D1 (`dpop_proofs`), so another isolate refuses it; cleaned up by the `clean_dpop_proofs` job                                 |
| `sign_in_required`   | 403 for every caller, with `WWW-Authenticate: <scheme> error="sign_in_required"`. The webshop maps on the `error` member, not the status                   |
