# The Shopify GraphQL Admin API from Python

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

## Overview {#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 features such as bulk operations never had a REST path at all. Building anything new on REST is throwaway work.

## Getting the access token {#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 complete authentication setup for a backend integration. There is no OAuth flow, 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 {#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 is now stored 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.

| Field | Value |
| --- | --- |
| Name | Shopify |
| Address | https://your-shop.myshopify.com/admin/api/2025-07/graphql.json |
| Security | The 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](https://shopify.dev/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 {#running-queries}

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

```python
# -*- 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 {#running-mutations}

Mutations also use .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](https://zato.io/docs/dev/shopify/graphql-error-handling.html) covers the full pattern.

```python
# -*- 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 {#shopifys-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 {#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 {#configuration-as-yaml}

Both the connection and the security definition can be defined in YAML with [enmasse](https://zato.io/docs/admin/enmasse.html) and imported per environment, with the token supplied through an environment variable.

## Learn more {#learn-more}

- [Shopify integration overview](https://zato.io/integrations/shopify/) - Start here to see how teams build Shopify integrations
- [Products and variants](https://zato.io/docs/dev/shopify/products-and-variants.html) - productSet, productVariantsBulkUpdate and the 2048-variant model
- [Rate limits and backoff](https://zato.io/docs/dev/shopify/rate-limits-and-backoff.html) - The cost-based leaky bucket, the 1,000-point ceiling and THROTTLED
- [Reliable webhooks](https://zato.io/docs/dev/shopify/webhooks-python.html) - HMAC verification, fast acknowledgments and deduplication
- [GraphQL error handling](https://zato.io/docs/dev/shopify/graphql-error-handling.html) - Why 200 OK does not mean success - errors and userErrors
- [Bulk operations](https://zato.io/docs/dev/shopify/bulk-operations.html) - Full-catalog reads and writes, polled by the scheduler
- [ERP and inventory sync](https://zato.io/docs/dev/shopify/erp-inventory-sync.html) - Source of truth, idempotent publishing and nightly reconciliation
