---
title: Create a buyer-linked token
description: >-
  Turn a signed-in Shop customer's session into a buyer-linked token using the
  UCP delegated IdP flow, with Shop as the delegated identity provider.
source_url:
  html: 'https://shopify.dev/docs/agents/profiles/buyer-linked-tokens'
  md: 'https://shopify.dev/docs/agents/profiles/buyer-linked-tokens.md'
---

# Create a buyer-linked token

By default, a [token](https://shopify.dev/docs/agents/profiles/auth-and-rate-limiting#token) identifies only your agent. A token that also carries the identity of a signed-in Shop customer is a buyer-linked token.

This page shows how to turn a signed-in Shop session into a buyer-linked token using the UCP [delegated identity provider (IdP) flow](https://ucp.dev/draft/specification/identity-linking/), where Shop (`accounts.shop.app`) is the delegated IdP that Shopify trusts. Instead of sending the customer through a second browser authorization, your agent chains the existing Shop identity to Shopify.

**Caution:**

Your Dev Dashboard client secret is confidential. Run steps 2 and 3 and the helper below on a server you control. Never run them in browser or mobile code, where the secret can be recovered from shipped source or network traffic. Public clients can't complete this flow safely.

***

## Prerequisites

* [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard) client credentials (a client ID and secret) with a redirect URI configured. You reuse these same credentials to authorize customers with Shop in step 1.

***

## How it works

This flow chains the customer's existing Shop identity to Shopify, following [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693) and [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523).

Don't hard-code endpoints or provider details. First, resolve them at runtime using UCP's existing discovery protocols:

* **Shopify's authorization server**: Read the OAuth protected resource metadata of the resource server you're calling, for example Global Catalog (`catalog.shopify.com`). It points to `api.shopify.com`, Shopify's global authorization server.
* **Shop as the delegated IdP**: Read the `dev.ucp.common.identity_linking` capability in Shopify's UCP business profile. It identifies Shop as the delegated identity provider and gives its `auth_url`.
* **Shop's endpoints**: Read Shop's published [OAuth authorization server metadata](https://accounts.shop.app/.well-known/oauth-authorization-server). It gives Shop's `authorization_endpoint` and `token_endpoint`.

Avoid unnecessary network requests by observing the cache directives in the responses.

Then run three steps. The first authorizes the customer with Shop; the rest chain that identity to Shopify:

1. **Authorize with Shop.** Run a standard OAuth authorization code flow against Shop to get a Shop access token for the signed-in customer.
2. **Exchange at Shop.** Exchange the Shop access token at Shop's token endpoint for a short-lived JWT authorization grant, audience-restricted to Shopify.
3. **Redeem at Shopify.** Present that grant to Shopify's token endpoint to receive a buyer-linked token.

***

## Step 1: Authorize the customer with Shop

Run a standard OAuth 2.0 authorization code flow against Shop (`accounts.shop.app`) to get a Shop access token for the signed-in customer. Reuse the same client ID and secret from your [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard) app (Shop recognizes them) along with the redirect URI you configured there.

Discover Shop's `authorization_endpoint` and `token_endpoint` from its [authorization server metadata](https://accounts.shop.app/.well-known/oauth-authorization-server) rather than hard-coding them.

First, redirect the customer to Shop's `authorization_endpoint`. Include a `state` value to protect against CSRF:

## Authorization request

https://accounts.shop.app/oauth/authorize?response\_type=code\&client\_id={your\_client\_id}\&redirect\_uri={your\_redirect\_uri}\&scope=openid%20dev.ucp.shopping.catalog.search:read\&state={state}

Percent-encode each parameter value. The space separating the two `scope` values is encoded as `%20`, and you must encode `{your_redirect_uri}` and `{state}` when you substitute them.

After the customer approves, Shop redirects to your redirect URI with an authorization code in the `code` parameter. Exchange it for a Shop access token at Shop's `token_endpoint`:

##### cURL

```bash
curl --request POST \
  --url https://accounts.shop.app/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code={authorization_code}' \
  --data-urlencode 'redirect_uri={your_redirect_uri}' \
  --data-urlencode 'client_id={your_client_id}' \
  --data-urlencode 'client_secret={your_client_secret}'
```

##### {} Response

```json
{
  "access_token": "{shop_access_token}",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

***

## Step 2: Exchange the Shop token for an authorization grant

Discover Shop's `token_endpoint` from its [authorization server metadata](https://accounts.shop.app/.well-known/oauth-authorization-server), then send a token-exchange request. Set the audience to Shopify's authorization server so the grant can only be redeemed at Shopify. This is a token-endpoint request, so authenticate it with the same client ID and secret you used in step 1:

##### cURL

```bash
curl --request POST \
  --url https://accounts.shop.app/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode 'subject_token={shop_access_token}' \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
  --data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:jwt' \
  --data-urlencode 'audience=api.shopify.com' \
  --data-urlencode 'client_id={your_client_id}' \
  --data-urlencode 'client_secret={your_client_secret}'
```

##### {} Response

```json
{
  "issued_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "access_token": "{jwt_authorization_grant}",
  "token_type": "N_A",
  "expires_in": 60
}
```

The `access_token` returned here is the JWT authorization grant. It's a signed, single-use, short-lived assertion, not an access token. Its only purpose is to be redeemed in the next step.

***

## Step 3: Redeem the grant for a buyer-linked token

Present the grant to Shopify's token endpoint as a JWT bearer assertion, requesting only the scopes you need. Authenticate the request with the same client ID and secret:

##### cURL

```bash
curl --request POST \
  --url https://api.shopify.com/auth/access_token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data-urlencode 'assertion={jwt_authorization_grant}' \
  --data-urlencode 'scope=dev.ucp.shopping.catalog.search:read' \
  --data-urlencode 'client_id={your_client_id}' \
  --data-urlencode 'client_secret={your_client_secret}'
```

##### {} Response

```json
{
  "access_token": "{buyer_linked_token}"
}
```

Shopify verifies the grant's signature against Shop's published keys, checks its claims, resolves the Shop customer, and issues a buyer-linked token. Like an app-only token, it's a JWT that expires after 60 minutes. Buyer-linked tokens aren't refreshed. When one expires, repeat these steps to mint a fresh token from the customer's Shop session.

***

## End-to-end example

The following helper resolves every endpoint through the discovery protocols, then performs steps 2 and 3 to return a buyer-linked token, starting from the Shop access token you obtained in step 1 and your Dev Dashboard client credentials. Discovery starts from the resource server you're calling. We are using the Global Catalog (`catalog.shopify.com`) as an example:

##### buyer\_linked\_token.js

```javascript
// Server-side only: this helper uses the confidential client secret and must
// never run in browser or mobile code.

// Discovery starts from the resource server you're calling.
const RESOURCE_SERVER = 'https://catalog.shopify.com';

async function getJson(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`${res.status} fetching ${url}`);
  return res.json();
}

// Discovery 1: find Shopify's global authorization server from the resource
// server's OAuth protected resource metadata, then read its token endpoint.
async function discoverShopifyAuthServer() {
  const { authorization_servers } = await getJson(
    `${RESOURCE_SERVER}/.well-known/oauth-protected-resource`,
  );
  const issuer = authorization_servers[0]; // https://api.shopify.com
  const { token_endpoint } = await getJson(
    `${issuer}/.well-known/oauth-authorization-server`,
  );
  return { audience: new URL(issuer).host, tokenEndpoint: token_endpoint };
}

// Discovery 2: find Shop as the delegated IdP from the identity linking
// capability in Shopify's UCP business profile.
async function discoverShopAuthUrl() {
  const { capabilities } = (await getJson(`${RESOURCE_SERVER}/.well-known/ucp`)).ucp;
  const [identityLinking] = capabilities['dev.ucp.common.identity_linking'];
  const [provider] = Object.values(identityLinking.config.providers)
    .flat()
    .filter((p) => p.type === 'oauth2');
  return provider.auth_url; // https://accounts.shop.app
}

// Discovery 3: read Shop's OAuth endpoints from its authorization server
// metadata. Its authorization_endpoint drives the step 1 browser redirect.
async function discoverShopEndpoints(shopAuthUrl) {
  return getJson(`${shopAuthUrl}/.well-known/oauth-authorization-server`);
}

// Step 2: exchange the Shop access token for a JWT authorization grant (RFC 8693).
async function getAuthorizationGrant({ tokenEndpoint, audience, shopAccessToken, clientId, clientSecret }) {
  const res = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
      subject_token: shopAccessToken,
      subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
      requested_token_type: 'urn:ietf:params:oauth:token-type:jwt',
      audience,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });
  const { access_token } = await res.json();
  return access_token; // the JWT authorization grant
}

// Step 3: discover every endpoint, then redeem the grant for a buyer-linked token (RFC 7523).
export async function getBuyerLinkedToken({ shopAccessToken, clientId, clientSecret, scope }) {
  const shopify = await discoverShopifyAuthServer();
  const shopAuthUrl = await discoverShopAuthUrl();
  const { token_endpoint: shopTokenEndpoint } = await discoverShopEndpoints(shopAuthUrl);

  // Step 1 (omitted): run a standard OAuth authorization code flow against
  // Shop's authorization_endpoint (from discovery 3) to obtain `shopAccessToken`
  // for the signed-in customer. See "Step 1: Authorize the customer with Shop" above.

  const assertion = await getAuthorizationGrant({
    tokenEndpoint: shopTokenEndpoint,
    audience: shopify.audience,
    shopAccessToken,
    clientId,
    clientSecret,
  });

  const res = await fetch(shopify.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion,
      client_id: clientId,
      client_secret: clientSecret,
      scope,
    }),
  });
  const { access_token } = await res.json();
  return access_token; // buyer-linked token, valid ~60 minutes
}
```

##### cURL

```bash
# Discovery starts from the resource server you're calling.
RESOURCE_SERVER="https://catalog.shopify.com"

# Discovery 1: Shopify's global authorization server, from the resource
# server's OAuth protected resource metadata.
SHOPIFY_ISSUER=$(curl --silent "$RESOURCE_SERVER/.well-known/oauth-protected-resource" \
  | jq -r '.authorization_servers[0]')
SHOPIFY_TOKEN_ENDPOINT=$(curl --silent "$SHOPIFY_ISSUER/.well-known/oauth-authorization-server" \
  | jq -r '.token_endpoint')
AUDIENCE=$(echo "$SHOPIFY_ISSUER" | sed -E 's#^https?://##')

# Discovery 2: Shop as the delegated IdP, from the identity linking capability.
SHOP_AUTH_URL=$(curl --silent "$RESOURCE_SERVER/.well-known/ucp" \
  | jq -r '[.ucp.capabilities["dev.ucp.common.identity_linking"][0].config.providers[][] | select(.type == "oauth2") | .auth_url][0]')

# Discovery 3: Shop's token endpoint, from its authorization server metadata.
SHOP_TOKEN_ENDPOINT=$(curl --silent "$SHOP_AUTH_URL/.well-known/oauth-authorization-server" \
  | jq -r '.token_endpoint')

# Step 1 (omitted): run a standard OAuth authorization code flow against Shop's
# authorization_endpoint to obtain $SHOP_ACCESS_TOKEN for the signed-in customer.
# See "Step 1: Authorize the customer with Shop" above.

# Step 2: exchange the Shop access token for a JWT authorization grant (RFC 8693).
GRANT=$(curl --silent --request POST \
  --url "$SHOP_TOKEN_ENDPOINT" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode "subject_token=$SHOP_ACCESS_TOKEN" \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
  --data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:jwt' \
  --data-urlencode "audience=$AUDIENCE" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET" \
  | jq -r '.access_token')

# Step 3: redeem the grant for a buyer-linked token (RFC 7523).
curl --silent --request POST \
  --url "$SHOPIFY_TOKEN_ENDPOINT" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data-urlencode "assertion=$GRANT" \
  --data-urlencode 'scope=dev.ucp.shopping.catalog.search:read' \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET"
```

##### {} Response

```json
{
  "access_token": "{buyer_linked_token}"
}
```

***

## Next steps

[Auth and rate limiting\
\
](https://shopify.dev/docs/agents/profiles/auth-and-rate-limiting)

[See how the Token, Signed, and Anonymous traffic tiers work.](https://shopify.dev/docs/agents/profiles/auth-and-rate-limiting)

[Global Catalog MCP\
\
](https://shopify.dev/docs/agents/catalog/global-catalog)

[Search products across all Shopify merchants with personalized results.](https://shopify.dev/docs/agents/catalog/global-catalog)

***
