> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Shopify

> Client-mode REST API connector for the Shopify GraphQL Admin API: adjust inventory from sensor counts with idempotent inventoryAdjustQuantities, publish open order pick lists to MQTT, look up inventory by SKU, list locations, and get client-credentials access tokens

Technical Preview

## Overview

A client-mode connector for the [Shopify GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql) (version `2026-10`). Use it to connect the shop floor to your online store: when a counter or scale reports finished goods, adjust the sellable stock in Shopify, and push the list of open, unfulfilled orders to MQTT so a packing line or pick-to-light system can work through it.

Every GraphQL event is a `POST` to `/admin/api/2026-10/graphql.json`. The event only decides the MQTT topics; the GraphQL document and its variables travel in the payload as `body`. Canonical bodies for each event are below.

| Event | Request | Notes |
| - | - | - |
| GetShop | `POST /admin/api/2026-10/graphql.json` (`shop`) | Smoke and store check |
| GetLocations | `POST …/graphql.json` (`locations`) | Location ids (`gid://shopify/Location/…`) for inventory changes |
| GetInventoryItems | `POST …/graphql.json` (`inventoryItems`) | Find an inventory item by SKU, with `available` and `on_hand` per location |
| AdjustInventoryQuantities | `POST …/graphql.json` (`inventoryAdjustQuantities`) | Add or remove stock by a delta, with an idempotency key |
| GetOrders | `POST …/graphql.json` (`orders`) | Open, unfulfilled orders with line items (pick list), paged by cursor |
| ClientCredentialsToken | `POST /admin/oauth/access_token` | Exchanges client id and secret for a 24-hour access token |

Topics use `finance/shopify/<object>/<verb>`; replies land on `<topic>/response`. Set `BASE_ADDRESS` to your store's permanent `https://<store>.myshopify.com` domain (not a custom storefront domain).

See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes).

> [!NOTE]
>
> **Auth:** every GraphQL call sends the `X-Shopify-Access-Token` header with `access_token` from the payload. Get a token in one of these ways:
>
> * **Dev Dashboard app, client credentials.** Create an app in the [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard/get-api-access-tokens) in the same Shopify organisation as the store, install it with the scopes below, and publish its client id and secret to ClientCredentialsToken. The token lasts 24 hours (`expires_in` 86399). This only works for stores in your own organisation; other stores answer `shop_not_permitted`.
> * **Existing admin-created custom app.** Its `shpat_…` Admin API access token does not expire. New admin-created custom apps can no longer be made.
> * **Public or custom-distribution app.** Run the authorization code grant outside the broker and pass the token.
>
> Scopes: `read_orders`, `read_inventory`, `write_inventory`, `read_locations` and `read_products`. Keep credentials in Hub secrets (suggested names `SHOPIFY_STORE`, `SHOPIFY_CLIENT_ID`, `SHOPIFY_CLIENT_SECRET`, `SHOPIFY_ACCESS_TOKEN`). Never commit them into the `.lot`.

> [!WARNING]
>
> **Idempotency is required.** From API version `2026-04`, `inventoryAdjustQuantities` must carry the `@idempotent(key: …)` directive. Use one key per physical count event (for example `line3-counter7-000123`) and send the same key when you retry, so a retried message never counts stock twice. Shopify remembers keys for 24 hours. Each change must also set `changeFromQuantity`: the quantity you expect before the change (Shopify rejects the change with `CHANGE_FROM_QUANTITY_STALE` if it moved), or `null` to skip that check.

> [!TIP]
>
> **GraphQL errors come back with HTTP 200.** Check the top-level `errors` array (syntax, `THROTTLED`, access denied) and the mutation's `userErrors` array (validation) in every reply. Only a bad token (**401**), a wrong store (**404**) and a few store states (**402**, **423**) change the HTTP status.

## Basic Syntax

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ShopifyAdminGraphqlApi WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://YOUR_STORE.myshopify.com"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '60'
        WITH DEFAULT_CONTENT_TYPE "application/json"
        # Auth: X-Shopify-Access-Token {value.json.access_token}; replace YOUR_STORE. Never commit tokens.
    ADD EVENT GetShop
        WITH SOURCE_TOPIC "finance/shopify/shop/get"
        WITH DESTINATION_TOPIC "finance/shopify/shop/get/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/admin/api/2026-10/graphql.json"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "X-Shopify-Access-Token" "{value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT GetLocations
        WITH SOURCE_TOPIC "finance/shopify/location/list"
        WITH DESTINATION_TOPIC "finance/shopify/location/list/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/admin/api/2026-10/graphql.json"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "X-Shopify-Access-Token" "{value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT GetInventoryItems
        WITH SOURCE_TOPIC "finance/shopify/inventory-item/list"
        WITH DESTINATION_TOPIC "finance/shopify/inventory-item/list/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/admin/api/2026-10/graphql.json"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "X-Shopify-Access-Token" "{value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT AdjustInventoryQuantities
        WITH SOURCE_TOPIC "finance/shopify/inventory/adjust"
        WITH DESTINATION_TOPIC "finance/shopify/inventory/adjust/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/admin/api/2026-10/graphql.json"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "X-Shopify-Access-Token" "{value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT GetOrders
        WITH SOURCE_TOPIC "finance/shopify/order/list"
        WITH DESTINATION_TOPIC "finance/shopify/order/list/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/admin/api/2026-10/graphql.json"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "X-Shopify-Access-Token" "{value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT ClientCredentialsToken
        WITH SOURCE_TOPIC "finance/shopify/oauth/token"
        WITH DESTINATION_TOPIC "finance/shopify/oauth/token/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/admin/oauth/access_token"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/x-www-form-urlencoded"
        WITH BODY "grant_type=client_credentials&client_id={value.json.client_id}&client_secret={value.json.client_secret}"
```

## MQTT examples

### Get a token (client credentials)

Publish to `finance/shopify/oauth/token`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "client_id": "<from SHOPIFY_CLIENT_ID>",
  "client_secret": "<from SHOPIFY_CLIENT_SECRET>"
}
```

Reply on `finance/shopify/oauth/token/response`: `{"access_token":"…","scope":"read_inventory,write_inventory,…","expires_in":86399}`.

### Find the inventory item for a SKU

Publish to `finance/shopify/inventory-item/list`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<from SHOPIFY_ACCESS_TOKEN>",
  "body": {
    "query": "query InventoryBySku($q: String!) { inventoryItems(first: 5, query: $q) { nodes { id sku tracked inventoryLevels(first: 10) { nodes { location { id name } quantities(names: [\"available\", \"on_hand\"]) { name quantity } } } } } }",
    "variables": {
      "q": "sku:CF-SEAL-01"
    }
  }
}
```

The reply gives the inventory item id, and for each location its id and the `available` and `on_hand` quantities. Use these ids in the adjustment. `GetLocations` lists all locations (`{"query":"query Locations($first: Int!) { locations(first: $first) { nodes { id name isActive } } }","variables":{"first":20}}`).

### Adjust stock from a sensor count

Publish to `finance/shopify/inventory/adjust` (here: 5 finished units counted on line 3):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<from SHOPIFY_ACCESS_TOKEN>",
  "body": {
    "query": "mutation AdjustFromSensor($input: InventoryAdjustQuantitiesInput!, $idempotencyKey: String!) { inventoryAdjustQuantities(input: $input) @idempotent(key: $idempotencyKey) { inventoryAdjustmentGroup { id createdAt reason referenceDocumentUri changes { name delta quantityAfterChange } } userErrors { field message code } } }",
    "variables": {
      "idempotencyKey": "line3-counter7-000123",
      "input": {
        "reason": "correction",
        "name": "available",
        "referenceDocumentUri": "gid://coreflux/SensorCount/line3-counter7-000123",
        "changes": [
          {
            "delta": 5,
            "inventoryItemId": "gid://shopify/InventoryItem/30322695",
            "locationId": "gid://shopify/Location/124656943",
            "changeFromQuantity": null
          }
        ]
      }
    }
  }
}
```

`delta` is the change, not the new total. Use a negative delta to remove stock. `reason` must be one of Shopify's reasons, for example `correction`, `cycle_count_available` or `received`. `referenceDocumentUri` shows up in the store's inventory history, so point it at your own system (`gid://coreflux/…`). A good reply has `"userErrors": []` and `quantityAfterChange` for each change.

### Publish the pick list

Publish to `finance/shopify/order/list`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<from SHOPIFY_ACCESS_TOKEN>",
  "body": {
    "query": "query PickList($q: String, $after: String) { orders(first: 25, after: $after, query: $q, sortKey: CREATED_AT) { nodes { id name createdAt displayFulfillmentStatus lineItems(first: 50) { nodes { sku name quantity } } } pageInfo { hasNextPage endCursor } } }",
    "variables": {
      "q": "status:open fulfillment_status:unfulfilled",
      "after": null
    }
  }
}
```

The reply lists open, unfulfilled orders with SKU, name and quantity for each line. When `pageInfo.hasNextPage` is `true`, publish again with `"after"` set to `pageInfo.endCursor`. `read_orders` only reaches recent orders; older ones need the `read_all_orders` scope.

### Smoke test

Publish to `finance/shopify/shop/get`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<from SHOPIFY_ACCESS_TOKEN>",
  "body": {
    "query": "query Shop { shop { name myshopifyDomain currencyCode } }",
    "variables": {}
  }
}
```

## Keeping the token

Client-credentials tokens last 24 hours. These two actions fetch a new one from Hub secrets twice a day and store it:

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ShopifyTokenFromSecrets
ON EVERY 12 HOURS DO
    SET "cid" WITH GET SECRET "SHOPIFY_CLIENT_ID"
    SET "csecret" WITH GET SECRET "SHOPIFY_CLIENT_SECRET"
    PUBLISH TOPIC "finance/shopify/oauth/token" WITH { "client_id": {cid}, "client_secret": {csecret} }

DEFINE ACTION ShopifyKeepToken
ON TOPIC "finance/shopify/oauth/token/response" DO
    SET "access" WITH GET JSON "access_token" IN PAYLOAD AS STRING
    IF {access} != "null" THEN
        KEEP SECRET "SHOPIFY_ACCESS_TOKEN" WITH {access}
```

Restrict who can read the `finance/shopify/#` topics with a rule, because these payloads carry tokens and client credentials.

## Webhooks (companion, not packaged)

Shopify can `POST` an `orders/create` [webhook](https://shopify.dev/docs/apps/build/webhooks/subscribe/https) to an HTTPS URL. Each delivery carries `X-Shopify-Hmac-SHA256`, a base64 HMAC-SHA256 of the raw body made with the app's client secret, and should be rejected if it does not match. A REST API connector in server mode cannot check that signature, so the receiver is not part of this template. Options: put a verifying proxy in front of a separate `ENABLE_SERVER` connector, use Shopify's Amazon EventBridge or Google Cloud Pub/Sub delivery, or poll GetOrders on a timer (for example with `created_at:>…` in `q`).

## Notes

* API versions are released every quarter and each is supported for at least 12 months. `2026-10` is accessible until October 2027. Change the version in each `ENDPOINT` when you upgrade; a retired version is answered by the oldest supported one.
* Rate limits use a calculated query cost in a leaky bucket per app and store; the size and restore rate depend on the store's plan. Each reply carries `extensions.cost`. On `THROTTLED`, wait and retry with the same idempotency key.
* Common errors: **401** `[API] Invalid API key or access token` (bad or expired token), `ACCESS_DENIED` in `errors` (missing scope), `userErrors` such as `CHANGE_FROM_QUANTITY_STALE` or an invalid reason, **404** (wrong store domain).
* Not covered: the REST Admin API (legacy), bulk operations, product and fulfillment mutations, `inventorySetQuantities` (sets an absolute quantity).
* Suggested branches: `feat/rest-api-route-shopify`, `feat/docs-rest-api-shopify` (Hugo pushes).

## Related

* [REST API Connectors](/latest/lot-language/routes/rest-api-routes)
* [Shopify: inventoryAdjustQuantities](https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryAdjustQuantities)
* [Shopify: orders query](https://shopify.dev/docs/api/admin-graphql/latest/queries/orders)
* [Shopify: implementing idempotency](https://shopify.dev/docs/apps/build/apis/graphql-admin/implementing-idempotency)
* [Shopify: client credentials tokens](https://shopify.dev/docs/apps/build/dev-dashboard/get-api-access-tokens)
* [Shopify API versioning](https://shopify.dev/docs/api/usage/versioning)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.