We’re implementing Customer Account API authentication for our Shopify app using the app-level client_id configured through [customer_authentication] in shopify.app.toml.
The authorization-code + PKCE flow works correctly. We receive a valid Customer Account API access token and ID token, and Customer Account API requests work as expected.
However, the token response does not contain a refresh_token.
Our authorization request uses:
response_type=code
client_id=<Shopify app client ID>
scope=openid email customer-account-api:full
code_challenge=<PKCE challenge>
code_challenge_method=S256
redirect_uri=<registered customer_authentication redirect URI>
We then exchange the authorization code at the discovered token_endpoint with:
grant_type=authorization_code
client_id=<same Shopify app client ID>
redirect_uri=<same redirect URI>
code=<authorization code>
code_verifier=<PKCE verifier>
The raw response from Shopify contains:
{
"access_token": "...",
"token_type": "bearer",
"expires_in": 3600,
"id_token": "..."
}
There is no refresh_token.
The access token itself contains the expected scope:
openid email customer-account-api:full
We have reproduced this with two separate Shopify apps, so it doesn’t appear to be specific to one app configuration.
Headless Customer Account client comparison
We also tested the same flow using the Customer Account API client ID from a Headless storefront on the same development shop.
With that client ID, the token response does contain a refresh_token.
So the observed difference is:
Shopify app client ID ([customer_authentication])
→ access_token
→ id_token
→ expires_in: 3600
→ NO refresh_token
Headless Customer Account API client ID
→ access_token
→ refresh_token
→ id_token
→ expires_in: 3600
This is particularly problematic for a native mobile app. With the Shopify app client ID, after the one-hour access token expires we have no refresh token available to obtain a new access token and therefore have to send the customer through the authorization flow again.
Reauthorization is relatively painless when the Shopify browser session still exists, but it still requires opening the authorization UI again instead of silently refreshing the Customer Account session.
The Customer Account API documentation appears to indicate that the authorization-code flow should return a refresh token and documents using it to refresh an expired access token.
Is the absence of refresh_token when using the Shopify app client ID with [customer_authentication] expected behavior?
If it is expected, is there another supported way to obtain a refresh token/persistent Customer Account session when using the app-level client ID?
Or is this a bug in the app-level Customer Account authentication flow?