# Shopify

The Omnata Shopify plugin is able to sync data inbound and outbound.

{% hint style="info" %}
The Omnata plugin uses the Shopify GraphQL Admin API, currently pinned to version `2026-04`.
{% endhint %}

On this page, you'll find:

* [Creating a connection](#authentication-methods)
* [Shopify's data structures](/omnata-product-documentation/omnata-sync-for-snowflake/apps/shopify/data-structure.md)

## Authentication methods

<table><thead><tr><th>Field</th><th>Required</th><th>Description</th><th data-hidden>Default</th></tr></thead><tbody><tr><td>Your Shopify Store URL</td><td>Yes</td><td>Your shop's <code>*.myshopify.com</code> URL. See <a href="#obtain-your-shop-name">Obtain your shop name</a>.</td><td>—</td></tr><tr><td>Admin API access token</td><td>Yes</td><td>The Admin API access token of the custom app you created in Shopify. Stored as a Snowflake secret. See <a href="#generate-your-api-access-token">Generate your API Access Token</a>.</td><td>—</td></tr><tr><td>User Tier</td><td>Yes</td><td>Your Shopify plan tier (Standard, Advanced, Shopify Plus, Enterprise). Determines the API rate limits Omnata applies. See <a href="#obtain-your-user-tier">Obtain your User Tier</a>.</td><td>—</td></tr></tbody></table>

The plugin supports two ways to authenticate to your Shopify store. New connections should use **Client Credentials** — it's the path Shopify supports for new apps created in 2026 and beyond. The legacy **Admin API access token** method is retained for stores that already have an access token issued from a custom app created before 1 January 2026.

* [Client Credentials (Client ID / Client Secret)](#client-credentials-client-id-client-secret) — recommended
* [API Access Token](#api-access-token) — legacy

Both methods share the same supporting fields: your shop URL and your User Tier. The next section explains those.

### Common connection fields

Regardless of which authentication method you choose, every connection requires these:

<table><thead><tr><th>Field</th><th>Required</th><th>Description</th><th data-hidden>Default</th></tr></thead><tbody><tr><td>Your Shopify Store URL</td><td>Yes</td><td>Your shop's <code>*.myshopify.com</code> URL. See <a href="#obtain-your-shop-name">Obtain your shop name</a>.</td><td>—</td></tr><tr><td>User Tier</td><td>Yes</td><td>Your Shopify plan tier. Determines the API rate limits Omnata applies. See <a href="#obtain-your-user-tier">Obtain your User Tier</a>.</td><td>—</td></tr></tbody></table>

#### Obtain your shop name

1. Visit your store page.
2. Click on **Settings** on the bottom left of your main store page.
3. Copy down your shop name from the URL of the Settings page (the part before `.myshopify.com`).

{% hint style="info" %}
Every Shopify store has a `*.myshopify.com` URL, even if you've configured a custom domain. Always use the `myshopify.com` form in the connection field.
{% endhint %}

#### Obtain your User Tier

1. In your Shopify admin, go to **Settings → Plan**.
2. Identify your current Shopify plan and choose the matching User Tier in the Omnata connection form:

| Shopify plan     | User Tier to select |
| ---------------- | ------------------- |
| Basic Shopify    | Basic               |
| Shopify          | Shopify             |
| Advanced Shopify | Advanced            |
| Shopify Plus     | Shopify Plus        |
| Enterprise       | Enterprise          |

The User Tier you choose drives the per-second and per-minute API rate limits Omnata applies — see [Managing performance](#managing-performance) for the values.

### Client Credentials (Client ID / Client Secret)

{% hint style="info" %}
**Recommended for all new connections.** This method follows Shopify's OAuth 2.0 client-credentials grant — Omnata exchanges your app's Client ID and Client Secret for a short-lived Admin API access token (24-hour lifetime) and automatically refreshes it.
{% endhint %}

#### Connection fields

In addition to the [Common connection fields](#common-connection-fields) above, this method requires:

<table><thead><tr><th>Field</th><th>Required</th><th>Description</th><th data-hidden>Default</th></tr></thead><tbody><tr><td>Client ID</td><td>Yes</td><td>The app's Client ID, copied from the Shopify Developer Dashboard.</td><td>—</td></tr><tr><td>Client Secret</td><td>Yes</td><td>The app's Client Secret. Stored as a Snowflake secret.</td><td>—</td></tr></tbody></table>

#### Steps to obtain a Client ID and Client Secret

These steps describe the high-level flow. Shopify updates the Developer Dashboard UI from time to time — refer to Shopify's [client credentials grant guide](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/client-credentials-grant).

1. Sign in to the [Shopify Developer Dashboard](https://shopify.dev/dashboard).
2. Create a new app. Give it a descriptive name (e.g. `Omnata Sync`).
3. Configure the app's **API access scopes**. We recommend granting read (and write, for outbound) access to every Shopify object you intend to sync — the scopes you'll need per stream are listed under [Generate your API Access Token](#generate-your-api-access-token) and apply identically here.
4. Install the app on the store you want to sync from. (Generate an installation URL from the dashboard and open it in a browser session for the store owner / admin.)
5. After installation, the app's overview page shows the **Client ID** and **Client Secret**. Copy both immediately and store the Client Secret somewhere safe — once revealed, it should be treated like a password.
6. Paste both values into the Omnata connection form, along with your shop URL and User Tier.

### API Access Token

**Legacy method.** Use this only if you already have an Admin API access token from a custom app created in the Shopify admin before 1 January 2026, or if you're connecting to a store where the Client Credentials grant is not viable.

{% hint style="warning" %}
As of 1 January 2026, Shopify [discontinued the creation of new custom apps directly from the Shopify admin](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/generate-app-access-tokens-admin). Stores that were already using a custom-app token before that date can continue to use it. For all new setups, prefer the [Client Credentials](#client-credentials-client-id-client-secret) method above.
{% endhint %}

#### Connection fields

In addition to the [Common connection fields](#common-connection-fields) above, this method requires:

| Field                  | Required | Default | Description                                                                                        |
| ---------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------- |
| Admin API access token | Yes      | —       | The Admin API access token (prefixed `shpat_`) from your custom app. Stored as a Snowflake secret. |

#### Generate your API Access Token

For stores that still have access to the legacy admin-side custom-app flow:

1. In your Shopify admin, go to **Settings → Apps and sales channels → Develop apps**.
2. Click **Allow custom app development** if prompted (on both screens).
3. Click **Create an app**, give it a name, then click **Create App**.
4. Open the **Configuration** tab, and under **Admin API Integration** click **Configure**.
5. Choose the access scopes you need. We recommend enabling read (and write, if you're using outbound) access for all the Shopify objects you intend to sync. The next section breaks down the minimum scopes per stream if you'd prefer to grant least-privilege access.

   <div data-gb-custom-block data-tag="hint" data-style="info" class="hint hint-info"><p>A missing scope for a single field will cause the whole stream to fail mid-sync, so err on the side of including a scope rather than excluding it.</p></div>

<details>

<summary>Scopes for inbound syncs</summary>

`customers`\\

* read\_customers (mandatory)

`products`\\

* read\_products (mandatory)
* read\_publications (for `availablePublicationsCount` field)

`orders`\\

* read\_orders (mandatory)
* read\_customer (for `customer` field)
* read\_products (for `line_items -> variant` field)
* read\_payment\_terms (for `paymentTerms` field)

`returns (dep. orders)`\\

* all scopes from `orders`
* read\_returns (mandatory)

`fulfillments (dep. orders)`\\

* all scopes from `orders`
* read\_fulfillments (mandatory)
* read\_locations (for `location` field)

`shippingLine (dep. orders)`\\

* all scopes from orders

`product_variants`\\

* read\_products (mandatory)
* read\_inventory (for `duplicateSkuCount` field)
* read\_shipping (for `duplicateSkuCount` field)

`locations`\\

* read\_locations (mandatory)

`shopifyPaymentsAccount`\\

* read\_shopify\_payments\_accounts (mandatory)

`balanceTransactions`\\

* read\_shopify\_payments\_accounts (mandatory)

`payouts`\\

* read\_shopify\_payments\_accounts (mandatory)

`shop`\\

* read\_products (for `allProductCategoriesList` field)
* read\_locations (for `location` field)
* read\_markets (for `marketWebPresence` field)
* read\_legal\_policies (for `shopPolicies` field)
* read\_locales (for `alternateLocales` field)

`inventory_items`\\

* read\_inventory (mandatory)

`inventory_levels (dep. inventory_items)`\\

* read\_inventory (mandatory)
* read\_locations (for `locations` field)

</details>

<details>

<summary>Scopes for outbound syncs</summary>

* Customers
  * write\_customers
* Products
  * write\_products
* Orders
  * write\_orders
* Fulfillment
  * write\_fulfilments

</details>

6. Click **Save**.
7. At the top right of the page click **Install app** and complete the installation.
8. After installation, an **Admin API access token** (prefixed `shpat_`) is generated. Copy it immediately — Shopify only shows it once.

{% hint style="info" %}
**API key vs Admin API access token.** Under **API Credentials** you'll see an **API key** and an **API secret key**. These are *not* the value you paste into Omnata's "Admin API access token" field — that's the `shpat_*` token shown on installation. The Admin API access token is not viewable again after the first reveal; store it somewhere safe (e.g. a password manager) in case you need to reconfigure the connection later.
{% endhint %}

***

## Inbound Syncs

All inbound syncs use Shopify's GraphQL Admin API. Each stream's primary key is `id` and uses a source-defined cursor.

| Stream                                                                                                                              | Sync strategies           | Depends on           | Notes                                                                                                                                                                          |
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`customers`](https://shopify.dev/docs/api/admin-graphql/latest/queries/customers)                                                  | Full Refresh, Incremental | —                    |                                                                                                                                                                                |
| [`products`](https://shopify.dev/docs/api/admin-graphql/latest/queries/products)                                                    | Full Refresh, Incremental | —                    |                                                                                                                                                                                |
| [`orders`](https://shopify.dev/docs/api/admin-graphql/latest/queries/orders)                                                        | Full Refresh, Incremental | —                    | Synced via the GraphQL bulk operations API.                                                                                                                                    |
| [`order_returns`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Return)                                                 | Full Refresh              | `orders`             | Returns attached to orders pulled in the current sync window.                                                                                                                  |
| [`order_shipping_lines`](https://shopify.dev/docs/api/admin-graphql/latest/objects/ShippingLine)                                    | Full Refresh              | `orders`             | Shipping line items attached to orders pulled in the current sync window.                                                                                                      |
| [`order_refund_adjustments`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Refund)                                      | Full Refresh              | `orders`             | Refund adjustment records attached to orders pulled in the current sync window.                                                                                                |
| [`order_fulfillments`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Fulfillment)                                       | Full Refresh              | `orders`             | Fulfillment records attached to orders pulled in the current sync window.                                                                                                      |
| [`order_fulfillment_line_items`](https://shopify.dev/docs/api/admin-graphql/latest/objects/FulfillmentLineItem)                     | Full Refresh              | `order_fulfillments` | Line items inside the fulfillments above.                                                                                                                                      |
| [`product_variants`](https://shopify.dev/docs/api/admin-graphql/latest/objects/ProductVariant)                                      | Full Refresh, Incremental | —                    |                                                                                                                                                                                |
| [`inventory_items`](https://shopify.dev/docs/api/admin-graphql/latest/objects/InventoryItem)                                        | Full Refresh              | —                    | Synced via the GraphQL bulk operations API.                                                                                                                                    |
| [`inventory_levels`](https://shopify.dev/docs/api/admin-graphql/latest/objects/InventoryLevel)                                      | Full Refresh, Incremental | `inventory_items`    |                                                                                                                                                                                |
| [`locations`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Location)                                                   | Full Refresh, Incremental | —                    |                                                                                                                                                                                |
| [`shop`](https://shopify.dev/docs/api/admin-graphql/latest/queries/shop)                                                            | Full Refresh              | —                    |                                                                                                                                                                                |
| [`paymens_account`](https://shopify.dev/docs/api/admin-graphql/latest/queries/shopifypaymentsaccount)                               | Full Refresh              | —                    | Shopify Payments account details. *(Note: the stream is currently named `paymens_account` — a typo in the source plugin. This is expected to be renamed in a future release.)* |
| [`balance_transactions`](https://shopify.dev/docs/api/admin-graphql/latest/connections/ShopifyPaymentsBalanceTransactionConnection) | Full Refresh              | —                    |                                                                                                                                                                                |
| [`payouts`](https://shopify.dev/docs/api/admin-graphql/latest/connections/ShopifyPaymentsPayoutConnection)                          | Full Refresh              | —                    |                                                                                                                                                                                |

### How dependent streams work

Streams marked as depending on another (e.g. `order_returns` depends on `orders`) are only populated for parent records that appear in the same sync. This means a Full Refresh of `order_returns` returns only the returns for orders fetched in that sync — pair it with a Full Refresh of `orders` if you need complete coverage.

### Bulk vs basic GraphQL operations

The plugin uses Shopify's [GraphQL bulk operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) for large streams and falls back to standard paginated GraphQL when the volume is below a threshold (currently 500 records). The behaviour per stream is:

| Mode         | Streams                                                                                                                                       | Behaviour                                                                                                                                                 |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Basic only   | `locations`, `shop`, `payments_account`, `balance_transactions`, `payouts`                                                                    | Always uses the standard paginated GraphQL API.                                                                                                           |
| Bulk only    | `orders`, `inventory_items`                                                                                                                   | Always uses the bulk operations API. If a tenant-wide bulk operation is already in progress, the plugin waits for it to finish before submitting its own. |
| Basic + bulk | `customers`, `products`                                                                                                                       | Uses bulk when the result set is above the threshold; falls back to basic GraphQL otherwise.                                                              |
| Dependent    | `order_returns`, `order_shipping_lines`, `order_refund_adjustments`, `order_fulfillments`, `order_fulfillment_line_items`, `inventory_levels` | Populated as a by-product of the parent stream's bulk operation — no independent API calls.                                                               |

### Handling Deletes

Shopify doesn't expose a general change-feed for hard-deleted records on most resources — once an object is deleted, it simply stops appearing in subsequent API responses. The Omnata Shopify plugin does not actively reconcile deletes during incremental syncs.

***

## Outbound Syncs <a href="#outbound-syncs" id="outbound-syncs"></a>

Supported Outbound sync strategies: Create, Update, Upsert, Delete.

You should structure your source data as described in [Outbound sync data structures](/omnata-product-documentation/omnata-sync-for-snowflake/apps/shopify/data-structure.md).

The following streams for these objects are supported:

* [customer](https://shopify.dev/docs/api/admin-rest/2024-07/resources/customer)
  * Create - Creates a new customer.
  * Update - Updates an existing customer's information.
  * Upsert - Creates a new customer or updates an existing customer's information.
  * Delete - Deletes an existing customer.

{% hint style="info" %}
Existing customers cannot be deleted if they have a pending order.
{% endhint %}

* [products](https://shopify.dev/docs/api/admin-rest/2024-07/resources/product)
  * Create - Creates a new product.
  * Update - Updates an existing product's information.
  * Upsert - Creates a new product or updates an existing product's information.
  * Delete - Deletes an existing product.
* [orders](https://shopify.dev/docs/api/admin-rest/2024-07/resources/order#resource-object)
  * Create - Creates an order.
  * Update - Updates an existing order's information.
  * Upsert - Creates an order or updates an existing order's information.
  * Delete - Deletes an existing order.
* [fulfillments](https://shopify.dev/docs/api/admin-rest/2024-07/resources/fulfillment#resource-object)
  * Create - Fulfills an existing unfulfilled order.
  * Update - Updates an existing fulfillment's information.
  * Upsert - Fulfills an existing unfulfilled order or updates an existing fulfillment's information.
  * Delete - Cancels an already fulfilled order.
* [product variants](https://shopify.dev/docs/api/admin-graphql/latest/objects/inventorylevel)
  * Create - Creates a product variant from on a product.
  * Update - Updates the product variant information.
  * Upsert - Creates a product variant if it does not exist or updates an existing's product variant's information.
  * Delete - Deletes a product variant from a product.
* [inventory item levels](https://shopify.dev/docs/api/admin-graphql/latest/objects/inventorylevel)
  * Create - Creates an inventory level at a specified location with the location id.
  * Update - Updates the inventory level stock amount.
  * Upsert - Creates an inventory level at a specified location with the location id or update the inventory level stock amount if the inventory level does not exist.
  * Delete - Deletes the entire inventory level.

{% hint style="info" %}
Inventory levels cannot be deleted if this is the last remaining inventory level of the product variant.
{% endhint %}

***

## Managing performance

### API rate limits per plan

The User Tier you select on the connection determines the per-second and per-minute REST request rate limits Omnata applies for the plugin. These values are aligned to Shopify's published [API rate limits](https://shopify.dev/docs/api/usage/rate-limits):

| User Tier    | Requests / second | Requests / minute |
| ------------ | ----------------- | ----------------- |
| Basic        | 2                 | 40                |
| Shopify      | 2                 | 40                |
| Advanced     | 4                 | 40                |
| Shopify Plus | 20                | 400               |
| Enterprise   | 40                | 400               |

If you upgrade or downgrade your Shopify plan, update the User Tier on the connection accordingly so Omnata throttles correctly.

### Bulk operations

Shopify has an API feature called bulk operations that offer much higher throughput than standard GraphQL queries. The Omnata plugin attempts to use these where possible to speed up sync times, falling back to regular queries where they cannot be executed.&#x20;

For streams and target objects that support both modes, the plugin attempts bulk operations:

* For inbound syncs, for each stream, if the expected record count for this sync is **at or above** `inbound_bulk_threshold` (default **500**).&#x20;
* For outbound syncs, for each target objects, when the number of records in the sync run is **at or above** `outbound_bulk_threshold` (default **50**).
* If below, it fall back to paginated GraphQL queries

#### Concurrency

Shopify allows a limited number of [bulk operations per shop](https://shopify.dev/docs/api/usage/bulk-operations/queries) at a time.&#x20;

If bulk operations are already in flight (started either by Omnata or by another integration), the plugin will wait for it to finish before submitting another. The maximum wait is 1 hour, after which the plugin falls back to the basic paginated API for streams that support both modes.

{% hint style="info" %}
**Contention with other integrations -** If another app on the same Shopify store is also running bulk operations, Omnata will see the same "already in progress" responses and follow the same wait-then-fallback behaviour. On busy shops you may see basic + bulk streams fall back to paginated GraphQL more often during peak periods.
{% endhint %}

#### **Disable bulk operations**

You can force paginated GraphQL on a basic + bulk stream by setting the `non_bulk_override` sync parameter to `true`. This applies regardless of record count. Bulk-only streams (`orders`, `inventory_items`) ignore it.&#x20;


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.omnata.com/omnata-product-documentation/omnata-sync-for-snowflake/apps/shopify.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
