Skip to Content
AI & AgentsAgentic CommerceIdentity Linking

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. Use linking when you need the buyer’s orders, or when you want the checkout attached to their customer.

Discover the authorization server

Both documents are public. They return 404 when UCP or identity linking is off for that domain.

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:

FieldValue
issuerhttps://{shop-domain}
authorization_endpointhttps://{shop-domain}/customer/authorize
token_endpointhttps://{shop-domain}/api/ucp/oauth/token
revocation_endpointhttps://{shop-domain}/api/ucp/oauth/revoke
response_types_supportedcode
code_challenge_methods_supportedS256
token_endpoint_auth_methods_supportednone
scopes_supportedThe 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.

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.

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.

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-._~]).

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}"
{ "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.

curl -sS -X POST "https://{shop-domain}/api/ucp/oauth/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=ucp_rt_…"

Scopes

ScopeGateWhat it does
dev.ucp.shopping.order:readRequired for order reads on a buyer tokenGET /api/ucp/orders/{id} for orders that belong to the linked customer
dev.ucp.shopping.checkout:manageOptionalAttaches 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:

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:

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.