The Shopify GraphQL Admin API from Python

Outgoing GraphQL connections, X-Shopify-Access-Token authentication, API version pinning, queries and mutations.

Overview

Zato connects to Shopify through an outgoing GraphQL connection. You create the connection once in the Dashboard - address, API version and access token - and every Python service can then obtain a client with self.out.graphql to run queries and mutations against your shop. The connection handles transport, timeouts and authentication, so no HTTP code and no token handling appear in your services.

GraphQL is the only choice for new work - Shopify marked the REST Admin API legacy in October 2024, new public apps have been GraphQL-only since April 2025, and surfaces such as bulk operations never had a REST path at all. Building anything new on REST is throwaway work.

Getting the access token

The token comes from a custom app in your Shopify admin. Go to Settings, then Apps and sales channels, then Develop apps, create an app, grant it the Admin API scopes your integration needs - for example read_products, write_products, read_orders - and install it into the shop. Shopify shows the Admin API access token once, a value starting with shpat_.

This is the whole authentication story for a backend integration. There is no OAuth dance, no App Bridge, no session tokens and no app-store review - those exist for apps embedded in the Shopify admin UI, which is a different thing than a server-side integration.

Creating the connection

First, store the token in an API-key security definition. In the Dashboard, go to Security -> API keys and create a definition whose header name is X-Shopify-Access-Token and whose value is the shpat_ token. The token now lives in one place, outside your code and outside version control.

Then create the connection itself - Connections -> Outgoing -> GraphQL - and attach the security definition to it.

FieldValue
NameShopify
Addresshttps://your-shop.myshopify.com/admin/api/2025-07/graphql.json
SecurityThe API-key definition with X-Shopify-Access-Token

The version segment in the address - 2025-07 above - pins your integration to one quarterly API release. Shopify supports each version for 12 months and breaking changes land every quarter, so an explicit pin plus a subscription to the developer changelog is what keeps upgrades a planned task instead of a production incident. Upgrading is a one-field change in the connection, with no code redeployment.

Running queries

Pass a query string to .execute(). Variables go into a params dict - never interpolate values into the query text yourself.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class FindProductBySKU(Service):
    name = 'shopify.find-product-by-sku'

    input = 'sku'

    def handle(self):

        conn = self.out.graphql['Shopify']

        query = """
            query FindBySKU($search: String!) {
                productVariants(first: 1, query: $search) {
                    edges {
                        node {
                            id
                            sku
                            price
                            product { id title }
                        }
                    }
                }
            }
        """

        params = {'search': f'sku:{self.request.input.sku}'}

        result = conn.execute(query, params=params)

        self.response.payload = result

Running mutations

Mutations work the same way - .execute() with a mutation string and variables. One thing is different from other GraphQL APIs you may know: a mutation that fails validation still returns HTTP 200 OK, with the details in a userErrors field inside the response. Always read it - the error handling guide covers the full pattern.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class AddProductTags(Service):
    name = 'shopify.add-product-tags'

    input = 'product_id', 'tags'

    def handle(self):

        conn = self.out.graphql['Shopify']

        mutation = """
            mutation AddTags($id: ID!, $tags: [String!]!) {
                tagsAdd(id: $id, tags: $tags) {
                    userErrors { field message }
                }
            }
        """

        params = {
            'id': self.request.input.product_id,
            'tags': self.request.input.tags,
        }

        result = conn.execute(mutation, params=params)

        # A 200 OK response can still contain validation errors
        user_errors = result['tagsAdd']['userErrors']

        if user_errors:
            raise Exception(f'Shopify rejected the mutation: {user_errors}')

Shopify's IDs

Every object is identified by a global ID such as gid://shopify/Product/1234567890. Mutations and lookups expect these full GIDs, not bare numbers - when you store Shopify IDs in your own database, store the whole GID string and the mapping problem disappears.

Several shops

One environment can talk to any number of shops - create one connection definition per shop, each with its own address and security definition, and pick the right one by name in your services.

Configuration as YAML

Both the connection and the security definition can be defined in YAML with enmasse and imported per environment, with the token supplied through an environment variable.

Learn more