# Identity Linking `dev.ucp.common.identity_linking` lets a buyer connect their store customer to your agent. The shop is the authorization server. Your agent is a **public** OAuth client: authorization code with PKCE (`S256` only). There is no client secret (`token_endpoint_auth_methods_supported` is `none`). - `client_id` is your platform profile URL, for example `https://your-platform.example/.well-known/ucp`. It must be `https` and must resolve to a UCP profile. - The issuer is the shop origin, `https://{shop-domain}`, the same host as `/.well-known/ucp`. Checkout does not require this flow. Start it from [Getting Started](./getting-started#checkout). Use linking when you need the buyer's orders, or when you want the checkout attached to their customer. ```mermaid flowchart LR Agent[Agent] Meta["/.well-known/oauth-authorization-server"] Consent["/customer/authorize"] Token["POST /api/ucp/oauth/token"] Order["GET /api/ucp/orders/id"] Agent --> Meta Agent --> Consent Consent --> Token Token -->|"Bearer ucp_at_…"| Order ``` ## Discover the authorization server Both documents are public. They return `404` when UCP or identity linking is off for that domain. ```bash curl -sS "https://{shop-domain}/.well-known/oauth-authorization-server" curl -sS "https://{shop-domain}/.well-known/oauth-protected-resource" ``` The authorization-server document includes: | Field | Value | |---|---| | `issuer` | `https://{shop-domain}` | | `authorization_endpoint` | `https://{shop-domain}/customer/authorize` | | `token_endpoint` | `https://{shop-domain}/api/ucp/oauth/token` | | `revocation_endpoint` | `https://{shop-domain}/api/ucp/oauth/revoke` | | `response_types_supported` | `code` | | `code_challenge_methods_supported` | `S256` | | `token_endpoint_auth_methods_supported` | `none` | | `scopes_supported` | The scopes below | `grant_types_supported` always includes `authorization_code` and `refresh_token`. A store with an external identity provider may also list `urn:ietf:params:oauth:grant-type:jwt-bearer`. Use the document you fetched. Do not assume the extra grant. The protected-resource document sets `resource` and `authorization_servers` to the same origin. `WWW-Authenticate` challenges point at it with `resource_metadata`. ## Browser consent Send the buyer to `GET /customer/authorize` in a browser. The buyer must be signed in to the store. A signed-out `GET` redirects to the store login and returns to this URL. A signed-out `POST` is `403`. ```text GET /customer/authorize?response_type=code &client_id=https%3A%2F%2Fyour-platform.example%2F.well-known%2Fucp &redirect_uri=https%3A%2F%2Fyour-platform.example%2Fcallback &scope=dev.ucp.shopping.order%3Aread &state={state} &code_challenge={challenge} &code_challenge_method=S256 ``` `redirect_uri` is an absolute `https` URL, or `http` on a loopback host. `code_challenge` is the S256 challenge of your verifier. Request at least one scope. Unknown scopes are rejected. The consent page posts back to the same path. Echo the authorize parameters and include the page's `csrf_token`. `decision` is `approve` or `deny`. Any value other than `approve` is a denial. ```http POST /customer/authorize Content-Type: application/x-www-form-urlencoded decision=approve &csrf_token={csrf_token} &response_type=code &client_id=https://your-platform.example/.well-known/ucp &redirect_uri=https://your-platform.example/callback &scope=dev.ucp.shopping.order:read &state={state} &code_challenge={challenge} &code_challenge_method=S256 ``` Approval redirects to `redirect_uri` with `code`, `state`, and `iss` (the shop origin). Denial redirects with `error=access_denied`, `state`, and `iss`. The code is single-use. It is prefixed `ucp_ac_`. ## Exchange and refresh `POST /api/ucp/oauth/token` and `POST /api/ucp/oauth/revoke` take `application/x-www-form-urlencoded`. They do not use the channel API key. Responses are OAuth JSON, not a UCP envelope. Cache them with `Cache-Control: no-store`. Exchange the code. `grant_type` is `authorization_code`. Send the same `redirect_uri` and the PKCE `code_verifier` (43–128 characters from `[A-Za-z0-9-._~]`). ```bash curl -sS -X POST "https://{shop-domain}/api/ucp/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=ucp_ac_…" \ -d "redirect_uri=https://your-platform.example/callback" \ -d "code_verifier={verifier}" ``` ```json { "access_token": "ucp_at_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "ucp_rt_…", "scope": "dev.ucp.shopping.order:read" } ``` `expires_in` is the access-token lifetime in seconds. The default is 3600. Refresh with `grant_type=refresh_token` and `refresh_token`. The presented refresh token is consumed. The response is a new pair. A bad code or refresh token returns `400` with `error` `invalid_grant`. Revoke with `token` set to an access or refresh token. Revoking a refresh token invalidates every token for that identity. Unknown tokens still return `200`. ```bash curl -sS -X POST "https://{shop-domain}/api/ucp/oauth/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=ucp_rt_…" ``` ## Scopes | Scope | Gate | What it does | |---|---|---| | `dev.ucp.shopping.order:read` | Required for order reads on a buyer token | `GET /api/ucp/orders/{id}` for orders that belong to the linked customer | | `dev.ucp.shopping.checkout:manage` | Optional | Attaches the checkout to the customer and can prefill saved details | Checkout works as a guest. When identity linking is available and the shopping call has no buyer token, the API adds an info message with `code` `identity_optional`. Presenting a buyer token that includes `dev.ucp.shopping.checkout:manage` attaches the checkout to that customer. Without the scope, checkout still proceeds and the customer is not attached. ## One Authorization header Shopping calls and buyer calls do not share a header. Resolution order on `Authorization`: 1. `Bearer fq_secret_…` is the platform **token** tier (the channel API key). 2. `Bearer ucp_at_…` is the buyer **user** tier. 3. Signature headers, then anonymous, only when those tiers are enabled and no bearer of the two prefixes above was present. They are not combined. A request carries one bearer value. Sending `ucp_at_…` does not also apply `fq_secret_…`. A bad buyer token is `401`, not a fall-through to anonymous. The body message `code` is `identity_required`. The challenge realm is the issuer, not the string `ucp`: ```http WWW-Authenticate: Bearer realm="https://{shop-domain}", error="invalid_token", error_description="The access token is invalid, expired, or revoked", resource_metadata="https://{shop-domain}/.well-known/oauth-protected-resource" ``` A buyer token that is valid but missing `dev.ucp.shopping.order:read` is `403`. The body `code` is `insufficient_scope`. The challenge lists the required scope and the same protected-resource URL: ```http WWW-Authenticate: Bearer realm="https://{shop-domain}", error="insufficient_scope", scope="dev.ucp.shopping.order:read", resource_metadata="https://{shop-domain}/.well-known/oauth-protected-resource" ``` Fetch `resource_metadata` when you need to find the authorization server from that challenge. Path tables for the token endpoints are in the [REST API reference](/reference/ucp/rest-api#oauth).