I’m migrating a service layer adapter that used the deprecated REST API to the new GraphQL API.My clients install my public application in their stores and manage their products and variants in my (separate) backend.
This service layer adapter listens to the new products on my backend and syncs them on shopify. Roughly, the old procedure was:
def create_product(shop_token: str):
activate_session(shop_token)
product = shopify.Product()
product.title = "title"
product.published_scope = "global"
...
variant = shopify.Variant()
variant.option1 = "Default - DO NOT USE"
variant.price = "100.00"
...
product.variants = [variant]
product.save()
def create_variant(variant_list)
activate_session(shop_token)
for variant in variant_list:
variant = shopify.Variant()
...
create_metafields(variant)
variant.save()
Which is a very common pattern so no surprises here.I could port the product, variant and metafields creation and got it to look the same on the store admin. But noticed the publication is not done in the same request.After a good amount of digging I came on suggestions to use a mutation of this shape:
mutation = """
mutation PublishToCurrentChannel($productId: ID!) {
publishablePublishToCurrentChannel(id: $productId) {
userErrors {
field
message
}
}
}
"""
variables = {
"productId": f"gid://shopify/Product/{product_id}",
}
do_query(mutation, variables)
This fails to pubblish complaining about lack of access scopes:
Access denied for publishablePublishToCurrentChannel field. Required access: `write_publications` access scope. Also: The user must have a permission to create and edit products
Which is kind of confusing because I already set those scopes on my application install hook and the manager I use to wrap this do_query
call.
Scopes:
MAIN_APP_SCOPES ={
"read_orders",
"write_orders",
"read_products",
"write_products",
"read_publications",
"write_publications",
... # several others
}
Session setup before do_query
def get_session():
shopify.Session.setup(
api_key=current_app.config["SHOPIFY_API_KEY"],
secret=current_app.config["SHOPIFY_SHARED_SECRET"],
)
shopify_session = shopify.Session(
self.shop.shop,
"2025-07",
self.shop.token,
access_scopes=MAIN_APP_SCOPES,
)
shopify.ShopifyResource.activate_session(shopify_session)
self._session = shopify_session
return shopify_session
App install hook.
@adapter_blueprint.route("/", strict_slashes=False)
def index():
shop = request.args.get("shop")
scopes = MAIN_APP_SCOPES
redirect_uri = url_for("adapter.finalize", _external=True, _scheme="https")
shopify.Session.setup(
api_key=current_app.config["SHOPIFY_API_KEY"],
secret=current_app.config["SHOPIFY_SHARED_SECRET"],
)
session = shopify.Session(shop, SHOPIFY_API_VERSION)
url = session.create_permission_url(scope=scopes, redirect_uri=redirect_uri)
return redirect(url)
I’m at my wit’s end as most of the examples from the official docs just fail with syntax errors, or have missing key components.
I’ve built most of this flow following recommendations from this forum but I haven’t found THE right mutation to make the publication.
Some recent conversations also point to hard coded publications IDs, which I tired and also fails.
Is there some setup I’m missing?
Is my publish mutation wrong?
If the mutation is wrong, why it complains about permissions and not syntax?
My application is a public application on shopify plus. I’m in the understanding that this scope is supported on my plan.
Thank you all in advance.