Skip to main content

Create a buyer-linked token

By default, a 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, 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.


  • 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.

This flow chains the customer's existing Shop identity to Shopify, following RFC 8693 and RFC 7523.

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. 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.

Anchor to Step 1: Authorize the customer with ShopStep 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 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 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 --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}'
{
"access_token": "{shop_access_token}",
"token_type": "Bearer",
"expires_in": 3600
}

Anchor to Step 2: Exchange the Shop token for an authorization grantStep 2: Exchange the Shop token for an authorization grant

Discover Shop's token_endpoint from its authorization server metadata, 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 --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}'
{
"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.


Anchor to Step 3: Redeem the grant for a buyer-linked tokenStep 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 --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}'
{
"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.


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:

// 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
}
# 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"
{
"access_token": "{buyer_linked_token}"
}


Was this page helpful?