How can an existing app remove write_products but keep read_products with managed installs?

We’re moving an existing automation app from legacy OAuth to managed installs. When a merchant removes a task that needs product write access, other tasks may still need product read access.

How do we remove write_products while keeping read_products for existing installations? This works through our current OAuth flow, but we haven’t found the equivalent with managed installs.

Our dev tests show:

  • Existing write access survives the migration.
  • Requesting read_products while write is held returns granted-all.
  • Removing write_products afterward removes both scopes, and product reads fail.

For new grants, requesting read and write together works: we can later remove write and retain read. Doing that when write is already held doesn’t fix the existing grant.

Is there a supported way to handle existing installations without revoking access first and asking the merchant to grant read again? If they close the tab or decline that second request, their remaining tasks would lose access.

This is blocking our managed-install migration. Happy to share the minimal reproduction or run further tests.

@TerenceShopify is there someone on your team that can help with this? We are blocked and want to continue our migration, so that we make Shopify Sidekick extensions :slight_smile: Appreciate your help as always!

Still looking for guidance on this. I’ve simplified the question above. We can make this work for new grants, but haven’t found a supported way to remove write access while preserving read for existing installations. This is blocking our managed-install migration. Could someone from Shopify clarify the intended approach?

Hey Matt, you should configure the scopes like this:

[access_scopes]
scopes = "read_products"
optional_scopes = ["write_products"]

Existing installations retain write_products when it moves from required to optional. New installations receive read_products during installation, but your app must request write_products dynamically when needed.

To remove the retained write_products grant from an existing installation, call the appRevokeAccessScopes mutation for that store after deploying the configuration change:

mutation {
  appRevokeAccessScopes(scopes: ["write_products"]) {
    revoked {
      handle
    }
    userErrors {
      field
      message
    }
  }
}

Because read_products remains required, revoking optional write_products should preserve product-read access.

Thanks Terence! Both scopes need to stay optional because some shops don’t need product access at all.

This works for new grants: we request read_products and write_products together, then later remove write_products. Read access remains.

It doesn’t work for existing grants from legacy OAuth: after migrating to managed installs, we request read_products and Shopify says it’s granted. But removing write_products then removes both scopes, and product reads fail.

How can we make those existing grants behave like the new ones, without temporarily removing all product access or making read required for every shop?

I’ve updated my last reply to make the distinction clearer: this works for new grants. The edge case we’re stuck on is existing grants carried over from legacy OAuth.

We’ve tested this pretty extensively :slight_smile: Mechanic requests permissions based on each shop’s tasks, so we need merchants to be able to remove write access without breaking tasks that still need read access.

I think I’m following, but just to be sure: you want to request that the app loses write_products but keeps read_products but that’s not possible with how appRevokeAccessScopes works today - is that right?

Yep, the problem is stores that already have write access when we migrate. We ask for read_products first, and Shopify says it’s granted. But when we then remove write_products, they lose read access too.

For new grants, requesting read and write together works we can later remove write and keep read. Is there a way to get existing grants into that same state without a gap in read access?

Gotcha - we’re looking at a fix for this behavior.

Thank you very much! Looking forward to doing this migration, so we can get build a sidekick extension.

Hi Matt - we have a new API available in the 2027-01 release candidate API version that should address your needs.

The appDowngradeAccessScopes mutation will let apps “downgrade” from write_* to read_*.

Thanks Terence! Just tested this with an actual legacy OAuth installation, switched it to managed installs, then called the new mutation using the original token. It removed write_products, kept read_products, and reads still worked. That’s exactly what we needed :slightly_smiling_face:

No chance of getting this sooner eh? I guess we’ll need to wait until Jan to do the migration. Thanks again for the help!

Hey all, adding the changelog post to this thread.