My app is still in Draft status so I cant use expiring tokens?

I am trying to test my app by installing one of my development stores after making updates to handle expiring tokens. The app has passed all the automated checks now.

In my oauth callback, i try to first setup a fulfillment service using the access token I fetch, but when calling the code below…

Im getting a HTTP 403 calling Shopify GraphQL Admin API: {“errors”:"[API] Non-expiring access tokens are no longer accepted for the Admin API

const string serviceMutation = @"
mutation FulfillmentServiceCreate(
$name: String!,
$callbackUrl: URL!,
$trackingSupport: Boolean!,
$inventoryManagement: Boolean!
) {
fulfillmentServiceCreate(
name: $name
callbackUrl: $callbackUrl
trackingSupport: $trackingSupport
inventoryManagement: $inventoryManagement
) {
fulfillmentService {
id
serviceName
callbackUrl
location { id }
}
userErrors { field message }
}
}";

Hey @Randal_B The 403 you’re seeing is because of a recent enforcement change. As of April 1, 2026, all new public apps are required to use expiring offline access tokens. If your app’s distribution is set to public in the Partner Dashboard, that enforcement applies even while the app is in draft, and Shopify will reject non-expiring tokens with the 403 you’re seeing.

The fix is to add expiring=1 to your POST to /admin/oauth/access_token when you exchange the authorization code. So your token request body should include that parameter alongside client_id, client_secret, and code. Once you do that, the response will include expires_in, refresh_token, and refresh_token_expires_in fields in addition to the access_token.

You’ll also need to implement refresh logic since the access token now expires after about 60 minutes. Before it expires, POST to the same endpoint with grant_type=refresh_token and the refresh token to get a new pair. The offline access tokens docs cover the full flow including the refresh request format.

Another developer ran into this same issue and documented their fix here if you want more detail on the implementation side. Hope this helps get up and running!

Ok, this is frustrating - Copilot is hallucinating again. First it pulled out information that said to include access_mode=offline in my authorize url, then the next day it told me to take it out lol.

My setup:
[ Oauth endpoints ]
(1) App start - runs a check to see if the shop is setup or not, and if not then redirects to OAuth start
(2) OAuth start - builds the admin approval url
currently the url looks something like this:

    var authorizeUrl =
        $"https://{shop}/admin/oauth/authorize" +
        $"?client_id={Uri.EscapeDataString(clientId)}" +
        $"&scope={Uri.EscapeDataString(scopes)}" +
        $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" +
        $"&state={Uri.EscapeDataString(state)}";

(3) OAuth callback - this is the redirect url set in dev dashboard. It handles (a) checking/validating the shop (b) setting up a fulfillment location via admin api (c) registering webhooks via admin api (c) saving configuration in the backend

Step 3 above is where things are failing - due to the 403 error mentioned above.

I see this paramater (expiring) mentioned in the docs here… yet copilot said NO, dont do that….grrrr

Also, the call to fetch an access token currently is set like this:
var tokenEndpoint = $“https://{shop}/admin/oauth/access_token”;

    var payload = new
    {
        client_id = apiKey,
        client_secret = apiSecret,
        code = shopCode
    };

Based on what you said above, it should just include this additional parameter?

    `var payload = new
    {
        client_id = apiKey,
        client_secret = apiSecret,
        code = shopCode,
        expiring = 1
    };`

I will retry this and report back.

fyi… copilot hallucinating? lol

Donal-Shopify - reporting back now, that seemed to do the trick. Webhook registrations are working now via the admin api with the token that was fetched (and the response is sending a refresh token). Im already updated to handle the token refresh.
thanks!

1 Like