Permission Getting Removed - Insufficient Scope Issue

We are seeking clarification regarding a permission/scopes issue encountered with one of our merchants.

Merchant ID: LF5MJAE8HBJT4

We recently identified that the merchant’s webstore integration stopped functioning due to the following error:

SquareApiException while updating order [{"errors":[{"category":"AUTHENTICATION_ERROR","code":"INSUFFICIENT_SCOPES","detail":"The merchant has not given your application sufficient permissions to do that. The merchant must authorize your application for the following scopes: ORDERS_READ"}]}]

After asking the merchant to re-authorize the application permissions, the integration immediately started functioning correctly again.

What we are trying to understand is how a previously granted permission/scope (ORDERS_READ) became unavailable in the first place.

From our side:

  • Our platform only supports requesting/granting the required scopes during authorization.

  • We do not have any functionality or API flow implemented to remove or downgrade permissions/scopes for a merchant.

  • No intentional changes were made from our side related to Square permissions.

From the merchant side:

  • The merchant stated they did not manually revoke permissions or modify app access/settings in Square.

We would appreciate clarification on the following:

  1. Under what circumstances can an already granted scope become unavailable or removed?

  2. Can Square automatically invalidate, downgrade, or remove scopes due to token refreshes, account changes, app setting updates, reauthorization flows, security actions, or other platform-level events?

  3. Is there any audit/event history available that can show:

    • when the permission/scope changed,

    • what triggered the change,

    • and whether it was initiated by the merchant, the application, or internally by Square?

  4. Are there any logs or APIs available that developers can use to track authorization scope changes for connected merchant accounts?

We are trying to identify the root cause so we can properly explain the behavior to the merchant and proactively monitor for similar situations in the future.

Any guidance would be greatly appreciated.

Thanks for the detailed question! Since the merchant says they didn’t revoke manually (and assuming your application isn’t calling the Revoke endpoint), the most likely culprit is your token refresh flow.

Two quick questions to help narrow this down:

  1. Do you have a token refresh flow built out? (i.e., are you calling ObtainToken with grant_type: refresh_token to get new access tokens?)

  2. If so, are you passing a scopes field in that request? If scopes is included, the new token gets restricted to only those scopes, so if ORDERS_READ was omitted, that permission would be silently dropped. Omittingscopes entirely preserves all originally granted permissions.

Let us know and we can dig further from there!

Relevant Docs:
Refresh, Revoke, and Limit Scope of OAuth Tokens
OAuth Best Practices

Hi Ashley,

Thanks for the guidance. We reviewed both our Core API and Merchant Web code paths.

To answer your questions:

  1. Yes, we do have a token refresh flow. We call ObtainToken with grant_type=refresh_token to renew Square access tokens.

  2. No, we are not passing a scopes field in the refresh token request.

Our refresh request only includes:

  • client_id
  • client_secret
  • grant_type = refresh_token
  • refresh_token

We confirmed that no scopes parameter is included during refresh, so based on the documentation, the refresh flow should preserve all originally granted permissions.

We also checked the initial authorization flow in Merchant Web. The Square OAuth URL is generated with the configured scope list, and the live configuration currently includes ORDERS_READ in the requested scopes. We did not find any alternate Square authorization path with a reduced scope list.

Additionally:

  • We do not call the Revoke endpoint as part of any automatic flow.
  • We do not have any functionality in Core API or Merchant Web to remove, downgrade, or update Square OAuth permissions for a merchant.
  • The only relevant token maintenance flow is the refresh flow described above, and it does not pass scopes.

Given this, it does not look like our application is reducing the granted permissions during token refresh or reauthorization.

Could you please help us investigate from Square’s side whether there is any audit/event history for merchant LF5MJAE8HBJT4 showing why ORDERS_READ became unavailable? Specifically, we would like to know whether the scope change was triggered by the merchant, Square, an app configuration change, a security action, or any other platform-level event.

Thanks again for helping us narrow this down.

Thank you for the additional context. Could you confirm your Square app ID to help me sort through the logs on our side?

Hi Ashley,

App ID: sq0idp-TpHen_F96miOUr07zsUKTw

Thank you for confirming the app ID.

After checking our logs, it looks like the merchant revoked your application’s access on May 31, 2026 at 23:11:04 UTC. This appears to have been a manual action; someone signed in to the Square Dashboard from a browser and disconnected your app from the merchant’s account.

I don’t see any activity with your app ID and that merchant after that revocation. The merchant will need to complete the OAuth flow again to reconnect their Square account to your application.

Yes, Ashley, this happened after this issue happened, and they opted out of our systems. Due to the permission issues. We want to identify what happened before and make sure that doesn’t happen to any future merchants.

Ah I see, thank you for the clarification. Do you have a date/timestamp for when an error was received? I’m having trouble find it in our logs.

Hi Ashley,

We noticed that they had not received any orders after 23rd May.

After checking with the IMPL team, we found the following error logs:

2026-05-27 20:07:58,717 - [pool-71-thread-215 - POST /business/web/LF5MJAE8HBJT4/cart/checkout] - ERROR - [XCID:9a50a871-4af3-4987-915b-f8a68184fd82 channel:WEB_STORE ] - i.a.s.s.client.SquareupApiClientImpl - SquareApiException while updating order [{"errors":[{"category":"AUTHENTICATION_ERROR","code":"INSUFFICIENT_SCOPES","detail":"The merchant has not given your application sufficient permissions to do that. The merchant must authorize your application for the following scopes: ORDERS_READ"}]}]
2026-05-27 20:07:58,717 - [pool-71-thread-215 - POST /business/web/LF5MJAE8HBJT4/cart/checkout] - ERROR - [XCID:9a50a871-4af3-4987-915b-f8a68184fd82 channel:WEB_STORE ] - i.a.s.s.client.SquareupApiClientImpl - Unknown Square error code [INSUFFICIENT_SCOPES]
2026-05-27 20:07:58,719 - [pool-71-thread-215 - POST /business/web/LF5MJAE8HBJT4/cart/checkout] - INFO - [XCID:9a50a871-4af3-4987-915b-f8a68184fd82 channel:WEB_STORE ] - consumerApiRequestLogger - 146410 * Server responded with a response on thread pool-71-thread-215 - POST /business/web/LF5MJAE8HBJT4/cart/checkout

On May 28th, we had a meeting with the merchant and requested them to re-grant the Square permissions. Once the permissions were re-authorised, the services started working again successfully.

Hi @ashley-square, is there any update on this?

Hi @nHiRanZ,

Thanks for your patience! We were able to fully trace your app’s permission history for this merchant.

Here’s the timeline (UTC) for the orders-read (ORDERS_READ) scope on your app for this merchant:

  • Apr 24 & May 1 — full scopes granted :white_check_mark: had ORDERS_READ
  • May 26, 16:15 — merchant re-authorized your app with reduced scopes :cross_mark: ORDERS_READ was dropped (they kept ORDERS_WRITE)
  • May 28, 13:50 — re-authorized again with full scopes :white_check_mark: ORDERS_READ restored
  • May 31, 23:11 — merchant fully disconnected/revoked the app from their Square Dashboard

Root cause: The INSUFFICIENT_SCOPES error on May 27 was caused by the May 26 re-authorization, which became the active grant and no longer included ORDERS_READ — so any call requiring that scope failed until the May 28 re-auth added it back.

In other words, this was a merchant-side scope change during re-consent, not a Square-side or app-side removal of the permission. It may be worth checking with the merchant on how and why they re-authorized with fewer scopes on May 26 — and, going forward, your app can detect a reduced grant by inspecting the scopes returned during the OAuth flow and prompting for re-consent if a required scope is missing.

Happy to help further if anything’s unclear!

Hi @ashley-square, thanks for checking this.

A couple of follow-up questions from our side:

  1. Is there any way for a merchant to modify the scopes granted to an already-authorized application outside of the application’s OAuth flow? For example, through the Square Dashboard, Square Web Portal, Square App, or any other Square-managed interface.

  2. For the May 26, 16:15 re-authorization event where ORDERS_READ was removed, are you able to determine the origin of that action? Specifically, can you tell whether it was initiated from:

    • Our application’s OAuth authorization flow,

    • The Square Dashboard/Web Portal,

    • A Square mobile application,

    • Or another Square-managed interface?

Since we only have a single location where permissions are requested, understanding exactly where that re-authorization originated would help us determine how the merchant ended up with a reduced scope set.

Hi @ashley-square, is there any update on this?

Thanks for following up. We’re still investigating this on our end. My understanding is that the re-authorization would have required the seller to complete a new OAuth flow with a URL generated by your application, but I’m working to confirm that.

While I am not able to definitely tell from our logs if the re-auth was seller initiated via clicking a new OAuth link or through programmatic token refreshing on your app, our records show the app toggled between the same two fixed scope sets twice (5/01 and 5/26), each time dropping the identical group of 6 scopes incl. ORDERS_READ while keeping ORDERS_WRITE. This appears to be a programmatic pattern, and points more towards an issue with token refresh logic. The 5/26 reduced grant is what produced the 5/27 INSUFFICIENT_SCOPES, and it was cleared by a full re-auth on 5/28.

Our suggestion: audit your token-refresh code for a partial/hardcoded scopes parameter. Omitting scopes on refresh preserves all granted permissions; only pass it when intentionally down-scoping.

Thanks for looking into this. We traced the full path on our side, from where the merchant first grants permissions through to how we refresh the token, and here is everything relevant.

1. Where scopes are actually requested: our merchant web app builds the Square authorize URL.

This is the only place in our entire codebase where a scope value is set for Square OAuth. The link the merchant clicks is rendered here:

<a class="button-gray" style="margin-top: 0px; margin-bottom: 20px"
   href="${authUrl}/oauth2/authorize?scope=${authScope}&client_id=${clientId}&session=${authSessionEnabled}&state=${uuid}">Connect
    with <img src="${cdnBaseUrl}/img/square-logo-white.svg" alt=""> </a>

authScope comes from a controller that picks one of two fixed, hardcoded scope strings based on the app type the merchant is onboarding into:

model.addAttribute("authScope", getAuthScopeBasedOnAppType(appType));

private String getAuthScopeBasedOnAppType(String appType) {
    if (appType.contains("kiosk")) {
        return messageSource.getMessage("merchant.partner.app.orderahead.kiosk.oauth.scope", null, Locale.ENGLISH);
    } else {
        return messageSource.getMessage("merchant.partner.app.orderahead.oauth.scope", null, Locale.ENGLISH);
    }
}

Our production values for those two scope strings are:

merchant.partner.app.orderahead.oauth.scope = CUSTOMERS_READ,CUSTOMERS_WRITE,ITEMS_READ,ORDERS_WRITE,PAYMENTS_READ,PAYMENTS_WRITE,MERCHANT_PROFILE_READ,ORDERS_READ,DEVICE_CREDENTIAL_MANAGEMENT,PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS,LOYALTY_READ,LOYALTY_WRITE,INVENTORY_READ

merchant.partner.app.orderahead.kiosk.oauth.scope = CUSTOMERS_READ,CUSTOMERS_WRITE,ITEMS_READ,ORDERS_WRITE,PAYMENTS_READ,PAYMENTS_WRITE,MERCHANT_PROFILE_READ,PAYMENTS_WRITE_IN_PERSON,ORDERS_READ,DEVICE_CREDENTIAL_MANAGEMENT,PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS,LOYALTY_READ,LOYALTY_WRITE,INVENTORY_READ

Both include ORDERS_READ. The only difference between them is PAYMENTS_WRITE_IN_PERSON for the kiosk variant. We checked the history of this config and ORDERS_READ was added in 2020 and has not been touched since, so there is no code path on our side, in any app type, that would generate a Square authorize link without ORDERS_READ.

2. This authorize link is only followed when a merchant clicks Connect. It is not something we trigger programmatically. The redirect back from Square, with the resulting code, is handled here:

@RequestMapping(value = "authorize-square", method = RequestMethod.GET)
public String handlePartnerAuthorization(ModelMap model,
                                         HttpServletRequest request,
                                         @RequestParam(value = "code", required = false) String code,
                                         @RequestParam(value = "state", required = false) String state,
                                         @RequestParam(value = "error", required = false) String error) throws BusinessNotFoundException {
    ...
    RegisterOAuthMerchantResponse registerOAuthMerchantResponse = businessApiService.registerSquareBusinessViaOauth(code, UserDetailsService.getUserId(request));
    ...
}

That call passes the code down to our backend service, which exchanges it for a token with no scopes parameter of its own:

public TokenResponse obtainToken(String code) {
    try {
        SquareClient client = createClient();

        ObtainTokenRequest body = ObtainTokenRequest.builder()
                .clientId(clientId)
                .grantType("authorization_code")
                .clientSecret(clientSecret)
                .code(code).build();
        ObtainTokenResponse result = client.oAuth().obtainToken(body);

        return getTokenResponse(result);
    } catch (Exception e) {
        LOGGER.error("Exception while obtaining oauth token ", e);
        throw ApptizerApiExceptions.INTERNAL_SERVER_ERROR_EXCEPTION;
    }
}

3. Token refresh, separately, never touches scopes at all and runs on its own fixed schedule, unrelated to anything above:

@Override
public TokenResponse renewToken(String accessToken) {
    try {
        String clientId = squareupConfiguration.getClientId();
        String clientSecret = squareupConfiguration.getClientSecret();

        SquareClient client = createClient();

        ObtainTokenRequest requestBody = ObtainTokenRequest.builder()
                .clientId(clientId)
                .grantType("refresh_token")
                .clientSecret(clientSecret)
                .refreshToken(accessToken)
                .build();

        ObtainTokenResponse result = client.oAuth().obtainToken(requestBody);

        return getTokenResponse(result);
    } catch (SquareApiException e) {
        LOGGER.error("SquareApiException while renewing Token [{}] ", toJson(mapToSquareError(e)), e);
        throw ApptizerApiExceptions.INTERNAL_SERVER_ERROR_EXCEPTION;
    } catch (Exception e) {
        LOGGER.error("Exception while renewing oauth token", e);
        throw ApptizerApiExceptions.INTERNAL_SERVER_ERROR_EXCEPTION;
    }
}

public boolean handleTokenRenewalForBusinessUser(BusinessUserData businessUserData, boolean forceSync) {
    ...
    TokenResponse tokenRefreshResponse = squareupApiClient.renewToken(squareRefreshKey);
    updateMerchantBusinessesWithRenewedTokens(merchantBusinessList, tokenRefreshResponse);
    ...
}

So, end to end, the JSP link above is the only place in our system where a scope value is ever set for this integration. Both of our possible scope values include ORDERS_READ, they have not changed in years, and refresh never sends a scope value at all. Given that, we don’t see a code path on our end that would produce the reduced scope set you found for this merchant on May 26. Would you be able to share what scope value Square actually received on that request, so we can compare it against the two values above and confirm whether it originated from our authorize link or from a manual change on Square’s side?

Hi @ashley-square, is there any update on this?

Hi @ashley-square, any luck on this?

One clarification that may help: a reduced grant doesn’t necessarily require a seller to click through consent, the ObtainToken refresh call accepts an optional scopes field, and passing a subset there issues a token limited to only those scopes.

Our durable authorization records show your app’s grant for this merchant toggling between the same two fixed scope sets on both 5/01 and 5/26, each time dropping the identical group of 6 scopes (incl. ORDERS_READ) while keeping ORDERS_WRITE. That repeating pattern is worth investigating on your side.

Has this happened with any other merchants on your integration?

Happy to help if anything else surfaces.

Thanks for digging into your authorization records, this is genuinely useful.

We went back through this again given what you shared. A couple of things we ruled out on our side:

We checked whether our SDK could be sending an empty scopes array by default even without us setting it explicitly, since that could theoretically be misread as a reduction request. It cannot. The Square Java SDK we use serializes the field with NON_ABSENT inclusion, so when we never call .scopes(...), the field is omitted from the request entirely, not sent as an empty list.

We also checked whether any other service in our infrastructure might be independently refreshing this merchant’s token. We have three backend services that integrate with Square in some way, and only one of them ever calls Square’s OAuth token endpoint. The other two just read the already-refreshed token from that first service. So there is exactly one code path in our entire system capable of making a refresh_token request to Square, and as shown before, it never populates scopes.

Given that, we’re not able to reproduce the reduced grant from anything in our code. To help us dig further, could you share:

  1. The full list of the 6 scopes that were dropped in each event, so we can compare it precisely against our known scope sets.
  2. Whether your records show the 5/01 and 5/26 events as authorization_code grants (fresh consent) or refresh_token grants. That tells us which of our two flows to keep digging into.
  3. The exact timestamps of both events, so we can line them up against our own request logs for this merchant.

On your last question, we’re rolling out extra logging and internal monitoring soon specifically to catch INSUFFICIENT_SCOPES failures as they happen, rather than relying on a merchant report like this one. Until that’s in place we don’t have a reliable way to say whether this has affected other merchants, but once it’s live we’ll be able to check our history and confirm either way. We’ll follow up here once we have that data.