What does the Shopify Connection provide? 

Looking to connect Gadget and Shopify?

Follow our tutorials and learn how to connect Gadget and Shopify in just a few minutes!

  • All models in Shopify's REST API have their schemas mirrored as models in Gadget. These models can be extended in Gadget with additional fields to store extra information, though these fields will not sync back with Shopify.
  • Each Shopify model syncs data from Shopify using webhooks and a daily background sync, as per Shopify's recommended best practices. You can also manually sync your app to Shopify at any time or trigger syncs with the Sync API.
  • Gadget sets up an already-authenticated API client package for making calls to Shopify. See Accessing the Shopify API
  • Gadget has easy-to-use facilities for billing your merchants

Currently, Gadget uses Shopify's 2023-01 API version for new connections.

Setting up the Shopify Connection 

If you are looking to connect Gadget with Shopify, follow the Connecting to Shopify tutorial.

Where is Shopify data saved 

All of the Shopify fields are mapped to a corresponding field in your Gadget model, and then these Gadget model records are updated when Shopify sends webhooks. You can identify these fields by looking at the schema for any of your app's Shopify models and finding any field with the Shopify icon.

A view of the schema editor for the Shopify product model

When a record in Shopify is changed, Shopify sends a webhook to your Gadget application, which triggers the appropriate model Action. A model's create webhook will fire the create model Action, update will fire update, and delete will fire delete. All of the incoming fields from the webhook are mapped and stored in their corresponding field in Gadget.

Syncs work much like webhooks in that they will create or update records in Gadget. However, Gadget currently does not delete any models that exist in Gadget but no longer exist in Shopify.

Gadget does not automatically sync data changed within Gadget back to Shopify. If you want to make changes to Shopify, you should make API calls to Shopify from within your Gadget Actions. See Accessing the Shopify API for more details.

Delete handling 

Any deletion webhooks will call the delete Action on the model, along with running the delete Action for all Shopify records associated with that model. For example, when a product is deleted, we delete all of its options, images, and variants. All other webhooks will call either the create or update Action, based on whether or not the record currently exists in Gadget.


Once set up, all of the models based on your selected scopes are now part of your app's GraphQL API, and you can query them like any other model in your app. You can change Shopify fields however you please, except for their type.

You can also add your own fields to these models. If you want to store extra data about each record from Shopify, Gadget will happily support adding a new field to the Shopify model with any of Gadget's supported field types, including relationships, and file fields.

Fields added to Shopify models can also be marked as metafields for syncing from Shopify.

Shopify model actions 

You can change the change, add, and remove Actions on Shopify connected models. Extending the existing Actions allows you to react to changes within Shopify, and adding your own actions allows you to define your own business logic, which can manipulate your own data or Shopify's in the context of one Shopify record.

For example, you can react to a Shopify product being created by adding a new Run Code Effect to the create action on the Shopify Product model:

module.exports = async ({ record, logger }) => {{ record }, "shopify product was just created");

Gadget will run the create Action for this model any time a product/create webhook arrives from Shopify or if a new Product record is discovered during a sync.

You can also add an Action to the Shopify Product model that runs your own business logic. For example, you could add an Action to update the product's title to be all uppercase. You would add an Action called upcase, and add one Run Code Effect to it with the following code:

1module.exports = async ({ record, connections }) => {
2 if (record.changed("title")) {
3 const shopify = connections.shopify.current;
4 await shopify.product.update(, { title: record.title.toUpperCase() });
5 }

This Action would be available to run on any Shopify Product record in your app.

Removing the defaults 

You can also remove the existing create, update, or delete actions from your models, but this will prevent the connection from being able to perform those actions in response to webhooks/syncs. For example, if you remove the delete action from the Shopify Product model, your Gadget database will no longer remove products when Shopify sends webhooks, preserving a long-term record of all products ever seen.

Gadget doesn't prevent you from deleting models, but doing so may prevent data from being updated. For example, if you have a product and product variant model, deleting the product model will mean that you no longer get updates to variant models. Deleting the product variant model is fine, and will not prevent products from being synced.

Key Shopify Models 

In addition to the models available to you based on your selected scopes, the Shopify connection creates three additional models: Shopify Shop, Shopify Sync, and Shopify GDPR Request.

Shopify Shop 

The Shopify Shop model is a representation of a Shopify shop connected to Gadget via the Shopify connection. It stores information about the connected shop, and a record is created when a shop is connected for the first time. You can create Code Effects on the creation of a Shopify Shop record that will then execute your logic on app installation.

Shopify Sync 

The Shopify Sync model represents the data sync between Shopify and Gadget for any given connected Shopify shop. Like the Shopify Shop model, you can create Code Effects on the creation of a sync record, which will execute whenever a sync is initiated between Shopify and Gadget.

Shopify GDPR Request 

The Shopify GDPR Request model represents requests from Shopify's mandatory webhooks in compliance with General Data Protection Regulation (GDPR). You can add custom Code Effects to the Shopify GDPR Request model's create Action to handle the request as needed, ensuring your app is GDPR compliant. To enable GDPR webhook requests from Shopify, go to the Apps page on the Shopify Partner Dashboard, select your application, click App setup, and paste<your-app-id> in the GDPR mandatory webhooks section.

Adding Code Effects 

Shopify models can be given business logic just like any other model in Gadget using Model Actions. Most often, developers add Run Code Effects to the Success Effect list for the create, update, or delete Actions on a Shopify model. These Effects can make calls to other APIs, create or change data in the Gadget app, or make calls back to Shopify. Within Effects, you have access to the current record that is being manipulated, the incoming params from the webhook, the api object for fetching or changing other data in your app, and the connections object for making API calls back to Shopify.

Note: Gadget's automatic data sync replicates changes made in Shopify to your Gadget app, but not the other way around. If you want to update Shopify data, you must make explicit API calls to the Shopify API to do so.

Let's say we wanted to build an automatic product tagger for Shopify products. When a product is changed in Shopify, our Gadget app should parse the product body, extract some new tags, and send them back to Shopify so customers can search for the product on the storefront easily. To do this, we can add a new Run Code Effect to the Shopify Product model's update Action that does this in code:

1const { extractKeywords } = require("easy-keywords");
3module.exports = async ({ api, record, params, logger, connections }) => {
4 if (record.changed("body")) {
5 const newTags = (await extractKeywords(record.body)).slice(20);
6 const allowedTags = (await api.allowedTag.findMany()).map(
7 (record) => record.tag
8 );
9{ newTags }, "saving new tags");
10 const shopify = await connections.shopify.forShopDomain(
11 ""
12 );
13 await shopify.product.update(, { tags: newTags.join(", ") });
14 }

The above code extracts tags from the product's body field using a module from npm, gets an authenticated API client for working with a specific Shopify shop's API, and then updates the product by making an API call back to Shopify using that client.

Accessing the Shopify API 

Looking to connect Gadget and Shopify?

Follow our tutorials and learn how to connect Gadget and Shopify in just a few minutes!

Gadget provides a helper for quickly accessing a client API object for communicating with Shopify. Gadget uses the shopify-api-node library as it is battle-tested, has good coverage of the Shopify API, and has support for automatic retries to avoid the Shopify API rate limits.

To access a Shopify API client, use the connections.shopify object in your Code Effect. connections.shopify is a helper object with some handy functions for accessing already-created API Client objects:

  • connections.shopify.current returns a Shopify API client for the current shop if there is one in context. For Public Shopify apps making requests from an embedded app, Gadget is able to track the shop making the request and populate this value.
  • connections.shopify.forShopId allows creating a Shopify client instance for a specific shop ID
  • connections.shopify.forShopDomain allows creating a Shopify client instance for a specific myshopify domain.

More info on the connections.shopify helper object can be found in the Code Reference for your application.

For example, we can use connections.shopify to access a Shopify client for a given store and then make a call to the Products REST API in Shopify to create a new product record:

1module.exports = async ({ connections }) => {
2 const shopifyClient = await connections.shopify.forShopDomain(
3 ""
4 );
5 await shopifyClient.product.create({
6 title: "New Product",
7 body_html: "This is the latest product on The Store",
8 tags: ["product", "new"],
9 });

If you'd prefer to use another client, the options attribute of the provided client has all the necessary details to construct one:

1import MyShopifyClient from "my-shopify-client";
3module.exports = async ({ connections }) => {
4 const shopifyClient = await connections.shopify.forShopDomain(
5 ""
6 );
7 const myClient = new MyShopifyClient({
8 myshopifyDomain: shopifyClient.options.shopName,
9 accessToken: shopifyClient.options.accessToken,
10 });
11 // ...

When to make API calls to Shopify in Code Effects 

For reading data within your effects, API calls can be placed anywhere without issue. That said, reaching out to Shopify can be expensive and can exhaust your Shopify API rate limit, so it is generally preferable to read data from your Gadget database instead of right from Shopify. Your Gadget database is a performant, nearby copy of the Shopify data expressly designed to power your business logic. It works well for this, but if you need the most up-to-date data possible, making an API call to Shopify works too.

For writing data within your Effects, API calls should generally be placed within Success Effects. Success Effects run after the Run Effects have all been completed without errors, which gives more confidence that your business logic has worked and saved changes to the database. This makes it less likely that your system and the external system will get out of sync. Gadget, by default, will execute the Run Effects of your app's Actions inside a database transaction. When the Run Effects fail for these actions, Gadget undoes (rolls back) any changes made to your app's database during the Action. Gadget cannot roll back changes made to external systems, so if you have made API calls within a failing Run Effect list, the side effects of those API calls will remain. Care should be taken to make API calls to external systems in the right place.

Managing Shopify API rate limits 

Shopify has restrictive rate limits, limiting apps to making 2 requests per second to any given shop. To avoid errors from hitting this rate limit, Gadget recommends two strategies:

  1. Use the shopify-api-node object returned by Gadget's connections.shopify helper, which is preconfigured to retry when encountering rate limit errors. shopify-api-node will retry a request up to 12 times, respecting Shopify's Retry-After header. If you need to make a write request to Shopify or a read of the most up-to-date data possible, this is the best option.
  2. Read data from your Gadget database where possible. Gadget's Shopify Connection syncs data from Shopify to your Gadget database so you can read quickly and without rate limits.

Read more about Shopify's rate limits here:

Current shop tenancy in Code Effects 

As the frontend is making queries and performing mutations on the data in your Gadget application, it is important to make sure that cross-talk between different shops is not allowed. This is especially relevant for public Shopify apps.

The context of the current shop is determined by the user and session making the request, or, if the Effect is triggered by a webhook or sync, the current record that is being processed.

You can use connections.shopify.current to gain access to the current shop's information. It is preferred to use connections.shopify.current in Code Effects to prevent any cross-talk between shops. You can then use the shopify-api-node API off of the current shop to interact with the current Shopify store.

1module.exports = async ({ api, record, params, logger, connections }) => {
2 if (connections.shopify.current) {
3 await connections.shopify.current.product.update(, {
4 tags: "foo, bar, baz",
5 });
6 }

It can also be useful to understand how filtered model permissions work in Gadget, as they determine what default filters, such as shopId in the case of a public app, are applied to GraphQL queries and mutations made through your application's API.

Current shop tenancy in HTTP routes 

If you are looking to get the current shop's context in a custom HTTP route being called from an app embedded in the Shopify admin, you can use your Gadget client's api.connection.fetch method. You can then access the current shop id in your Gadget route.

Global Actions - a possible alternative

You may be able to use a Global Action instead of an HTTP route. One advantage to Global Actions is that they are included in a Gadget project's generated GraphQL API, so shop tenancy for an embedded app is handled automatically. For more differences between Global Actions and routes, see Global Actions vs HTTP routes.

Here's an example of how we can do this in a React component from an embedded Shopify app:

1// this is your Gadget project's API client
2import { api } from "../api";
4export default function MyReactComponent() {
5 const [data, setData] = useState(null);
7 useEffect(() => {
8 // define an async function to make the request
9 const customHttpRouteRequest = async () => {
10 // make sure to use your Gadget app domain and route!
11 const result = await api.connection.fetch("");
12 const json = await result.json();
13 // use state hooks to handle response data in your component
14 setData(json);
15 };
17 // call async function and handle errors
18 customHttpRouteRequest().catch(console.error);
19 }, []);
21 return (
22 <div>
23 <code>
24 <pre>{JSON.stringify(result, null, 2)}</pre>
25 </code>
26 </div>
27 );

The current Shopify store id can be accessed in a Gadget route using request.connections.shopify.current. The current session is available in a route file with request.applicationSession.

1module.exports = async (request, reply) => {
2 const currentSession = request.applicationSession;
3 const currentShopConnection = request.connections.shopify.current;
4 const currentShopId = request.connections.shopify.currentShopId;
6 reply.code(200).send({ currentSession, currentShopId, currentShopConnection });

The current shop connection can be used to make API requests against the Shopify store that sent the request to the route.

By default, the Gadget API request.api object will be able to read model records across shops. You can use the current shop id as a filter if you only need a single shop's data in your route code.

1module.exports = async (request, reply) => {
2 const currentShopId = request.connections.shopify.currentShopId;
4 const products = await request.api.shopifyProduct.findMany({
5 filter: {
6 shop: {
7 equals: currentShopId,
8 },
9 },
10 });
12 reply.code(200).send({ products });

Webhook infinite loop handling 

It is common for Gadget Code Effects running in response to a webhook to make calls to Shopify, which then trigger the same webhook a second time. In the example above, we're processing an update Action on a product record, and within it, we update the same product record again using the Shopify API. This triggers a second product/update webhook, which will dispatch the same Gadget Action and run the Effects again. Without care, this can cause infinite webhook loops, which chew through your Shopify API rate limit and Gadget resources.

To avoid webhook infinite loops, you must ensure that your code doesn't re-process the changes it makes and re-trigger webhooks forever. The best strategy for detecting this is to only trigger your business logic if the fields you care about have changed on the record instead of always doing it when any field changes. The record object in the Effect parameters is a GadgetRecord object which reports which fields have been changed by the incoming webhook payload, and which have not.

In the example above, we only need to extract new tags for the product when the product's body field has changed. If anything else changes, we don't need to change the tags, as they purely come from the body, so we wrap our business logic in a check for if (record.changed("body")). This way, when we make the API call to update the product's tags in Shopify, Gadget will receive this webhook and dispatch the update Action, but our code won't run twice, as record.changed('body') will be false, as we only updated the record's tags.

More details on the GadgetRecord changes API can be found here.

Shop installs 

You can click Shop Installs to view the shops that have installed your application. This page also allows you to manually Sync data per store or view Sync History.

A view of the Installs page with two successfully installed stores

You can click the Sync button to sync a single store. Expanding the options for a single store allows you to view the Sync History or to Register Webhooks.

'View Sync History' and 'Register Webhooks' drop down options for a single store on the Installs page

You can also perform operations on multiple stores at once by selecting multiple stores and then clicking Sync or Register Webhooks buttons.

Displays available bulk operations on the on the Installs page, 'Sync' and 'Register Webhooks'

The Installs page also lets you know if you are missing access scopes on your store or if you are missing topics/namespaces for your registered webhooks.

To fix missing access scopes, the merchant who installed the application needs to grant access to the scope in the Shopify Dashboard.

Warning message on a store missing 2 scopes and a pop over detailing the missing scopes

In most cases, selecting Register Webhooks from the store options button or clicking Register Webhooks with multiple stores selected should fix missing webhook topics. If you are editing a connection that includes new models, you need to grant additional scopes before the new webhooks can be registered. The Register Webhooks button will also not handle missing checkouts_create and checkouts_update webhooks at this time.

Warning message on a store missing multiple webhook topics and a pop over listing the topics

There are two ways to associate a Shopify store with a custom app:

  1. Through a merchant install link

  2. Via the "Test on development store" link in the application view on your Partners Dashboard.

Both of these will start Shopify's installation flow , where the user has to authorize your app to be able to access its data. Once authorized, your connection is now associated with that store. You should see the shop's name and domain on the connections index page, and the sync button should now be available so that you can get the shop's data into Gadget. You can repeat the above process if you need to connect to another store, which you would typically do to allow installation on both a development store and the associated merchant store.

Editing Shopify Connections 

You can also edit your connections using the Edit button on the Connections page. This allows for changes to be made to the Shopify scopes and models that your application can access and import.

The section where a user updates Shopify scopes and models

Adding new scopes 

If you have a custom application built through the Partners dashboard or Shopify CLI and edit your connection to require additional scopes, you will need to re-authenticate your app for all stores on which it is installed. To do this, you can go through the app installation process again. This involves re-installing your app on development stores in the Partners Dashboard.

If you are building an embedded application using React, and use the Gadget Provider from our @gadgetinc/react-shopify-app-bridge package, reauthentication will be handled for you.

If you have an application built through a store admin, you must ensure that the scopes selected in the admin match the scopes required by your Gadget connection. If they are out of sync, navigate to the Shop Installs page. From there, you can click the Sync button to the right of the store you are trying to sync. You may also need to reregister your webhooks.

The refresh scopes menu option of a Shopify admin connection

Adding new models without new scopes 

You might edit your connection to only add new models, not new scopes. In this case, you do not need to reauthenticate or reinstall, but you may need to register new webhooks for your newly selected models. You can check to see if you need to register webhooks on the Installs page for your connection.

For example, you might have a connection set up that has the read_products scope selected along with the Product model. If you edit your connection to include the Collection model, you do not need new scopes, but you need to register the collections/create, collections/update, and collections/delete webhooks manually.

Screenshot of the Installs page with a store that has a missing webhooks warningScreenshot of the Installs page with a store that has a missing webhooks warning expanded to show missing Collections webhooks

To register webhooks, you need to go to the Installs page for your Connection in Gadget, click the More button (…) for any stores you have re-authenticated on and select the Register Webhooks option.

Screenshot of the Register Webhooks option for a store on the Installs page

Once the webhooks are registered, you are ready to continue building your app using the newly selected scopes and models.

Upgrading the Shopify API version 

Shopify releases a new API version every 3 months at the beginning of the quarter, and each stable version is supported for a minimum of 12 months. This generally means that a given version will become unsupported 12 months after its release. Shopify recommends upgrading your apps to make requests to the latest stable API version every quarter. Gadget makes this process simple by allowing you to edit your Shopify Connection's API version.

To upgrade the API, go to the Connections page and select your Shopify Connection. There you will see the current API version and a link to upgrade.

Screenshot of a Shopify connection with the API version upgrade link

Click on Upgrade and select the API version that you would like to upgrade to. Note that downgrading API versions is not currently supported.

Screenshot of API version upgrade page

Once an API version has been selected, the page will display:

  • Added - fields that have been added to models.
  • Deprecated - fields that Shopify has marked as deprecated in the API version.
  • Modified - an enum value has been added to a field or field type has changed.
  • Disconnected - fields that have been removed by Shopify which Gadget will now allow you to manage yourself. Gadget will never delete fields that Shopify has disconnected! However, these fields will no longer be updated via Shopify webhooks or the nightly sync.
Screenshot of added, deprecated, modified, and disconnected fields on API upgrade

Click on Upgrade and then Confirm to complete the API version upgrade.

Screenshot of confirmation modal to complete API version upgrade

You should ensure that the Event Version specified in your Partner Dashboard App Setup or Admin App Configuration page is set to the same version.

Screenshot of Partner Dashboard app setup event versionScreenshot of Admin App configuration event version

After upgrading your API version, you may wish to backfill existing records with new values by running a forced sync.

Lastly, if you are upgrading a Shopify API version that is 2022-07 or older to a version that is 2022-10 or newer, you will need to ensure that you request access to Protected Customer Data. See here for more details on how to request access.


The syncing system for the Shopify Connection works by leveraging Actions and Effects. Each time Gadget goes to sync the connected shop, Gadget creates a new Shopify Sync record and dispatches the Run and either the Complete or Error Actions based on the outcome of the sync. There are three types of syncs for the Shopify connection: API-triggered, Manual, and Daily.

Daily syncs 

The Shopify connection will automatically fetch all recent data in the background daily. This ensures we have all of the shop's data in the case of a missing webhook or for resources that don't have webhooks (such as the Online Store content). This sync only fetches data from the past day, so if the sync fails or has scheduling issues, there's a possibility you may not have the most up-to-date data in Gadget. If this does occur, check the Shopify Sync data viewer or logs via the Sync History to identify any errors.

API-triggered Syncs 

An API-triggered sync can be initiated via the GraphQL API, JS clients, or the api object in Code Effects. There are many ways that you can use API-triggered syncs. The three main ways are after a shop installation, on a model's Action, and syncing data within a specified date range.

Sync on shop install 

It may be necessary to run an initial sync upon installation. For example, you may want to display all products on a user interface immediately after a merchant installs your app. To do this, a Success Effect can be added to the Shopify Shop model's create Action with the following Code Effect:

1module.exports = async ({ api, record, params, logger, connections }) => {
2 await{
3 shopifySync: {
4 domain: record.domain,
5 shop: {
6 _link:,
7 },
8 },
9 });
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
{ "shopifySync": { "shop": { "_link": "SHOPID" }, "domain": "SHOPDOMAIN" } }

Sync by model 

You can optionally pass a models parameter to the runSync call to specify which particular Shopify models to sync. For example, if you wish to sync all products in your backend, but not orders, the models should be passed as an array of model API identifiers. If the models parameter is omitted or is an empty array, all Shopify models will be synced.

1module.exports = async ({ api, record, params, logger, connections }) => {
2 if (record.shopId) {
3 const shop = await api.shopifyShop.findOne(record.shopId);
4 await{
5 shopifySync: {
6 domain: shop.domain,
7 shop: {
8 _link: record.shopId,
9 },
10 // optional parameter
11 models: ["shopifyProduct"],
12 },
13 });
14 }
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
2 "shopifySync": {
3 "shop": { "_link": "SHOPID" },
4 "domain": "SHOPDOMAIN",
5 "models": ["shopifyProduct"]
6 }

Sync by date-time 

You may want to limit syncing to recent records. To do this, you can pass the syncSince parameter to indicate which records Gadget should sync. Gadget will check the shopifyUpdateAt field for each Shopify record, and only sync records created or updated within the specified window. Without this parameter, the API-triggered sync will copy every record to Gadget, regardless of when it was created or updated.

1module.exports = async ({ api, record, params, logger, connections }) => {
2 if (record.shopId) {
3 const shop = await api.shopifyShop.findOne(record.shopId);
4 const syncSince = new Date();
5 const millisecondsPerDay = 1000 * 60 * 60 * 24;
6 // sync from 5 days ago
7 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
8 await{
9 shopifySync: {
10 domain: shop.domain,
11 // optional parameter
12 syncSince,
13 shop: {
14 _link: record.shopId,
15 },
16 },
17 });
18 }
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
2 "shopifySync": {
3 "shop": { "_link": "SHOPID" },
4 "domain": "SHOPDOMAIN",
5 "syncSince": "ISOSTRING"
6 }

Forced syncs 

A sync will not update any fields or run any Actions if the shopifyUpdatedAt value in the payload is less than or equal to the shopifyUpdatedAt value currently stored on the record. There are certain situations, such as Shopify API version upgrades, where you may want to backfill new values or re-run your Actions on existing records. In these situations, you can run a sync with the force field set to true. A forced sync will update all values and re-run all Actions on applicable records.

After connecting to Shopify, a globalShopifySync Global Action has been added to your Gadget app. You can use this Global Action to run a forced sync across all stores your app has been installed on. If you go to Global Actions, then select the globalShopifySync Action and click Test Action, you will be brought to the API Playground with the globalShopifySync mutation loaded for you. To run a forced sync, you need to include "force": true in your variables, as well as the API Key/Client Id of your connected Shopify app. You can also include syncSince and models variables to limit the scope of the sync.

Variables for the globalShopifySync mutation
"apiKeys": ["<your Shopify app's API key>"],
"force": true

You can also run a forced sync on an individual store in JS or GraphQL by writing a custom action:

1module.exports = async ({ api, record, params, logger, connections }) => {
2 if (record.shopId) {
3 const shop = await api.shopifyShop.findOne(record.shopId);
4 const syncSince = new Date();
5 const millisecondsPerDay = 1000 * 60 * 60 * 24;
6 // sync from 5 days ago
7 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
8 await{
9 shopifySync: {
10 domain: shop.domain,
11 // optional parameter
12 syncSince,
13 shop: {
14 _link: record.shopId,
15 },
16 force: true,
17 },
18 });
19 }
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
2 "shopifySync": {
3 "shop": { "_link": "SHOPID" },
4 "domain": "SHOPDOMAIN",
5 "syncSince": "ISOSTRING",
6 "force": true
7 }

Manual syncs 

A sync will be queued (without a syncSince set) when clicking the "Sync" button next to your Shopify Connection. This will make a call to the Shopify Sync model's Run Action.

The connections index page having performed a single sync successfully

This sync may take some time to complete, depending on how much data the shop has.

Sync error handling 

If an error occurs during a sync, the Shopify Sync model's Error Action will be called. For example, here's how you teach Gadget to alert you via SMS when a sync fails.

1const Twilio = require("twilio");
2// Environment variables set in Gadget
3const twilio = new Twilio(
4 process.env["TWILIO_ACCOUNT_SID"],
8module.exports = async ({ api, record, params, logger, connections }) => {
9 /*
10 params: {
11 id: string;
12 shopifySync: {
13 errorMessage: string;
14 errorDetails: string;
15 }
16 }
17 */
19 const errors = params.shopifySync.errorDetails.split("\n");
21 // send the errors individually to the logger
22 for (const error of errors) {
23 logger.error({ error }, "an error occurred syncing");
24 }
26 // send an SMS notification
27 await twilio.sendSMS({
28 to: "+11112223333",
29 from: "+11112223333",
30 body: `Shopify sync failed with message: ${params.shopifySync.errorMessage}`,
31 });
  • id is the Shopify Sync id that was created
  • errorMessage contains a generate description of the error or errors
  • errorDetails contains a all error messages (if more than 1 occurred) joined by a \n

The best way to debug why a sync may have failed is to search for errors through your logs. Use the following log query to filter by sync ID (replace {syncId} with ID of the sync that has failed):

{app_logs_service=~".+", environment_id="{environmentID}"} | json | connectionSyncId = "{syncID}"

Protected Customer Data Access 

You will need to ensure that you request access to Protected Customer Data if you are using one of the following models:

  • Shopify Balance Transaction
  • Shopify Cart
  • Shopify Checkout
  • Shopify Comment
  • Shopify Customer
  • Shopify Draft Order
  • Shopify Event
  • Shopify Fulfillment Order
  • Shopify Order

To request access, go to your Partner Dashboard App Setup and find the section labeled Protected customer data access. See Shopify's documentation for more details.

Screenshot of Partners app configuration protected customer data access request
Missing customer field data?

If your app needs access to fields such as customer name, email, phone number, or address, you need to make sure you request individual field access in the optional Protected customer fields section of the customer data access page. If you do not select the individual fields you need access to, Shopify will not send this data via sync or webhook, and the records in your Gadget app database will have null or empty values instead.

A screenshot of the protected customer data access page, focused on the Protected customer fields section for the customer name field

Impersonating a Shopify App User 

Impersonating a user refers to simulating a user's behavior to test your application's functionality. Bugs that users encounter may not be so straightforward in the logs and hard to trace in code. Impersonating a user is a great way to test your backend logic.

You may notice that the connections.shopify.current object is undefined when you are testing your Code Effects from the API Playground. By default, requests made using the API Playground have admin access to your Gadget API but have the unauthenticated role and are not linked to a shopifyShop record. To impersonate a user in the GraphQL Playground, you must follow these steps:

  1. Open a new private (incognito) browser window and navigate to Don't close this window; you will need it later.
  2. Run the following query to retrieve the current session id of the incognito window:
query {
currentSession {
  1. From a public (not incognito) browser window, navigate to the API Playground and use the following mutation to link the session record, returned in the private browser window, with the Shopify shop you wish to impersonate:
1mutation {
2 internal {
3 updateSession(
5 session: { shop: { _link: "SHOPID" }, roles: ["Role-Shopify-App"] }
6 ) {
7 success
8 session
9 }
10 }

Once you have completed the last step, you can impersonate the user with the API Playground in the private browser window that was opened earlier.


Shopify metafields allow you to extend Shopify's data model to store additional information on resources, including orders, products, customers, and shop. Frequently used in custom and public app development, metafields are an important tool for building rich Shopify app experiences.

Despite their importance and popularity, working with metafields can be challenging for developers. The data is stored in a separate resource on Shopify's end (the “metafield object”), whereas you almost certainly want the data right on the resource you are extending with that metafield. Additionally, Shopify's API rate limits make this additional resource hopping even more cumbersome to navigate.

To simplify this experience, Gadget offers developers a fast way to sync, query, and mutate Shopify metafields in their app's backend.

Storing metafield data in Gadget 

Gadget allows you to store metafield data directly on any of the following Shopify resources: articles, blogs, collections, customers, orders, pages, products, and shop.

To do this, you must first copy the desired resource to your Gadget backend. Go to the Connections screen and pick the desired scopes and models (e.g. the read_products scope and the Product model), and connect to a store.

Once you have the desired model in your Gadget backend, click on Add Field to decorate the model with the additional metafield values. Name your field and select an appropriate type for the incoming data. To start the metafield import, you also need to click the Store data from Shopify Metafield box.

The required fields to set up a metafield on a Shopify Model

You now need to register your metafield's Namespace. Enter the Namespace that is used for your metafield in the Shopify Store, then click Register Namespace. Gadget will then register for webhooks on the entered Namespace.

The namespace for a metafield is registered

Once you get a message stating that the Namespace registrations are complete, you can enter the key for your metafield and select the correct metafield type.

Here's an example of what the form looks like when we try to teach Gadget to store a metafield capturing each product variant's weight under the Namespace "gadget_app" and key "spiciness":

A completed Shopify metafield setup in Gadget

Your Gadget app is now set up to store the data directly on this model. You can go back to your Connections page and sync to pull in metafield data. Once the metafield data is synced to your Gadget app, you can query or mutate metafield values using Gadget Actions.

Querying metafields 

One of the main advantages of Gadget's approach to metafields is that once you specify where the metafield values should be stored, you can query the data using Gadget's instant CRUD APIs.

You can fetch metafield values alongside the records they correspond to in a single query. You can also run range queries directly on the metafields.

Metafield query examples 

Here's an example of a range query being run on “spiciness”, a metafield stored on the Shopify Product, in order to fetch products that have a spiciness rating of greater than 5:

1query {
2 shopifyProducts(
3 filter: { spiciness: { greaterThan: 5 } }
4 sort: { spiciness: Ascending }
5 ) {
6 edges {
7 node {
8 title
9 id
10 spiciness
11 handle
12 }
13 }
14 }

Writing to metafields 

If you're looking to mutate metafield values in Gadget and sync them back to Shopify, you will need to teach Gadget about the mutation in code. You can do this in Gadget by writing Code Effects and having Gadget run them when the API is triggered.

Metafield writing examples 

Here's an example of a Code Effect added to the update Action of a Shopify Product model that sends the metafield information to Shopify:

2 * Effect code for update on Shopify Product
3 * @param { import("gadget-server").UpdateShopifyProductActionContext } context - Everything for running this effect, like the api client, current record, params, etc. More on effect context:
4 */
5module.exports = async ({ api, record, params, connections }) => {
6 if (record.changed("title")) {
7 await connections.shopify.current?.graphql(
8 `mutation updateAMetafield($metafields: [MetafieldsSetInput!]!) {
9 metafieldsSet(metafields: $metafields) {
10 metafields {
11 id
12 value
13 }
14 }
15 }`,
16 {
17 metafields: [
18 {
19 key: "a_metafield",
20 namespace: "test321",
21 ownerId: `gid://shopify/Product/${}`,
22 type: "single_line_text_field",
23 value: record.title + " as a metafield!",
24 },
25 ],
26 }
27 );
28 }

This will only update the metafield value in Shopify! If you have registered the metafield on a Gadget model, the webhook that fires after the metafield is updated in Shopify will update the metafield value in Gadget.

We have an example of a more dynamic metafield update available as a gist on Github.


Metaobjects are similar to metafields, but they don't need to be tied to a Shopify resource. A metaobject definition can take any shape - metaobject fields are made up of groups of metafield types and can include references to Shopify resources, such as Orders or Products, or even references to other metaobjects!

Metaobjects can be created in the Shopify admin or using the metaobject API. Created metaobjects can be accessed directly in Liquid, using the Storefront API, or in the Admin API.

Metaobjects in Gadget 

To read or write metaobjects you need to add the Metaobject and Metaobject Definitions scopes when setting up or editing your Shopify connection.

It's important to note that:

  • The metaobjects API is available in API version 2023-01 and above
  • There are no models to import for metaobjects
  • Webhooks for metaobjects do not exist!

If you want to sync metaobject data from Shopify to Gadget, you will need to do so manually:

  • Create custom models in Gadget that match your metaobject definitions used to store the metaobject data
  • A code effect that can be triggered manually, on a schedule, or as part of some other Action (ie. store install) that manually fetches metaobjects stored in Shopify. Note that you will need to deal with Shopify rate limits and add retry logic yourself.

Metaobject definitions 

You can use Shopify's GraphQL API to read, create, update, and delete metaobject definitions.

You can use your Gadget app's current Shopify connection to call these APIs to add or make changes to metaobject definitions on a merchant's store. For example, if you wanted to store some information about bikes (their style, model name, year, and a reference to the product record they pertain to), you would simply run the following code snippet:

1const metaobjectCreateMutation = `
2 mutation metaobjectDefinitionCreate($definition: MetaobjectDefinitionCreateInput!) {
3 metaobjectDefinitionCreate(definition: $definition) {
4 metaobjectDefinition {
5 name
6 type
7 }
8 userErrors {
9 field
10 message
11 }
12 }
13 }
14 `;
16// define a metaobject that can be used to store information for bicycles
17const createMetaobjectDefinition = await connections.shopify.current.graphql(
18 metaobjectCreateMutation,
19 {
20 definition: {
21 access: {
22 storefront: "PUBLIC_READ",
23 },
24 capabilities: {
25 publishable: {
26 enabled: true,
27 },
28 },
29 description: "Model information for bikes",
30 displayNameKey: "model",
31 fieldDefinitions: [
32 {
33 description: "Style of bike",
34 key: "style",
35 name: "Style",
36 type: "single_line_text_field",
37 required: true,
38 },
39 {
40 description: "Bike model name",
41 key: "model",
42 name: "Model",
43 type: "single_line_text_field",
44 required: true,
45 },
46 {
47 description: "Model year",
48 key: "year",
49 name: "Year",
50 type: "number_integer",
51 validations: {
52 name: "min",
53 value: "1950",
54 },
55 required: true,
56 },
57 {
58 description: "Bike product reference",
59 key: "bike_ref",
60 name: "Product reference",
61 type: "product_reference",
62 required: true,
63 },
64 ],
65 name: "Bike",
66 type: "bike",
67 },
68 }

Querying metaobjects 

Actual instances of metaobjects can be queried using the metaobject API. You can use the Shopify connection created in Gadget to create an instance of a bike metaobject. You must have a matching metaobject definition before you create instances of a metaobject.

1const metaobjectSeedDataMutation = `
2 mutation metaobjectCreate($metaobject: MetaobjectCreateInput!) {
3 metaobjectCreate(metaobject: $metaobject) {
4 metaobject {
5 handle
6 }
7 userErrors {
8 field
9 message
10 }
11 }
12 }`;
14// add a new instance of my "bike" metaobject
15const createMetaobjectSeedData = await connections.shopify.current.graphql(
16 metaobjectSeedDataMutation,
17 {
18 metaobject: {
19 type: "bike",
20 handle: "kona-dew-plus-2015",
21 capabilities: {
22 publishable: {
23 status: "ACTIVE",
24 },
25 },
26 fields: [
27 {
28 key: "style",
29 value: "hybrid",
30 },
31 {
32 key: "model",
33 value: "Dew Plus",
34 },
35 {
36 key: "year",
37 value: "2015",
38 },
39 {
40 key: "bike_ref",
41 value: "gid://shopify/Product/7965217390872",
42 },
43 ],
44 },
45 }

You can also use the metaobjects query to read metaobject data. The following example uses the metaobject type field to fetch instances of a "bike" metaobject:

1await connections.shopify.current.graphql(
2 `query GetMetaobjects($type: String!) {
3 metaobjects(type: $type) {
4 fields {
5 key
6 reference
7 type
8 value
9 }
10 id
11 type
12 displayName
13 }
15 { type: "bike" }

References to metaobjects 

Even though metaobjects are stored independently of Shopify resources, Shopify also allows you to make associations between a metaobject and existing resources, such as products or orders, through metaobject references.

Metaobject references are stored on records as metafields and contain a reference to the metaobject. To capture this relationship in Gadget, you need to add a new metafield to a Shopify model in Gadget and select the "Metaobject reference" type. This field will then contain a reference to the metaobject in Shopify.

Available models 

Gadget receives webhooks and syncs data from Shopify for the following models:

Model NameRequired ScopesWebhook Topics
AppAlways availablesynced only
App CreditAlways availablesynced only
Shopify App Credits (and other billing resources) are only available to OAuth apps created via the Shopify CLI or in the Shopify Partners dashboard that are marked for Public distribution. To successfully receive webhooks or sync this model, you must mark your app for Public distribution. Find more instructions in the Shopify docs.
App InstallationAlways availablesynced only
App Purchase One TimeAlways availableapp_purchases_one_time/update
Shopify App Purchase One Time (and other billing resources) are only available to OAuth apps created via the Shopify CLI or in the Shopify Partners dashboard that are marked for Public distribution. To successfully receive webhooks or sync this model, you must mark your app for Public distribution. Find more instructions in the Shopify docs.
App SubscriptionAlways availableapp_subscriptions/update
Shopify App Subscription (and other billing resources) are only available to OAuth apps created via the Shopify CLI or in the Shopify Partners dashboard that are marked for Public distribution. To successfully receive webhooks or sync this model, you must mark your app for Public distribution. Find more instructions in the Shopify docs.
App Usage RecordAlways availablesynced only
Blogread_contentsynced only
Shopify blogs support Metafields but do not provide a REST or GraphQL mechanism for syncing them. Gadget does not support Metafields on Shopify Blog models.
Articleread_contentsynced only
Shopify blog posts support Metafields but do not provide a REST or GraphQL mechanism for syncing them. Gadget does not support Metafields on Shopify Article models.
Commentread_contentsynced only
Themeread_themesthemes/create, themes/update, themes/delete
Gadget only syncs themes and assets that a store owns for each Shopify store, which excludes demo themes. Themes and assets for themes with role: "demo" in the Shopify API are not synced as API clients don't have permission to access individual asset values.
Assetread_themessynced only
Gadget syncs the Theme and Asset resources from Shopify using the REST API. Gadget does not sync the value column of the Asset resource. Syncing the value field can be done with a custom code effect, but Shopify requires one API call per asset to retrieve asset values, which often stresses Shopify API rate limits too much. Shopify Theme assets are often also quite large, including images and large JS files that are generally not important for applications to sync and store again.
Gadget recommends avoiding syncing the Asset and Theme model if possible, and instead making API calls directly to Shopify to work with assets using the connections.shopify.current API client.
Gadget only syncs themes and assets that a store owns for each Shopify store, which excludes demo themes. Themes and assets for themes with role: "demo" in the Shopify API are not synced as API clients don't have permission to access individual asset values.
Balance Transactionread_shopify_payments_payoutssynced only
Checkoutread_checkouts, read_orderscheckouts/create, checkouts/update
The read_checkouts scope will allow Gadget to process checkout related webhooks, but does not provide a mechanism to sync all existing checkouts.

Adding the read_orders scope, while also including the Checkout model in your application, will result in all Abandoned Checkouts that aren't handled via webhooks (failed delivery, checkouts that already exist) being imported during nightly or manual syncs.
Billing Addressread_checkouts, read_ordersSent within Checkout model
Checkout Applied Gift Cardread_checkouts, read_orderssynced only
Checkout Line Itemread_checkouts, read_ordersSent within Checkout model
Checkout Shipping Rateread_checkouts, read_orderssynced only
Shipping Addressread_checkouts, read_ordersSent within Checkout model
Bulk OperationAlways availablebulk_operations/finish
Carrier Serviceread_shippingsynced only
Cartread_orderscarts/create, carts/update
Cart Line Itemread_ordersSent within Cart model
Collectionread_productscollections/create, collections/update, collections/delete
Gadget uses the Collection model to represent both Custom Collections and Smart Collections from Shopify.
Collectread_productssynced only
Countryread_shippingsynced only
Provinceread_shippingsynced only
Customerread_customerscustomers/create, customers/update, customers/delete
Customer Addressread_customersSent within Customer model
Customer Payment Methodread_customer_payment_methodscustomer_payment_methods/create, customer_payment_methods/update, customer_payment_methods/revoke
Discount Coderead_discountssynced only
Discountread_discountssynced only
Disputeread_shopify_payments_disputesdisputes/create, disputes/update
Dispute data is only available for Shopify merchants using Shopify Payments.
Dispute Evidenceread_shopify_payments_disputessynced only
Dispute File Uploadread_shopify_payments_disputessynced only
Dispute Evidence Fulfillmentread_shopify_payments_disputessynced only
DomainAlways availabledomains/create, domains/update, domains/destroy
Shopify Domain delete and update webhooks are missing key data, so Gadget does an inline sync of the Domain REST API resource when these webhooks arrive to properly discover updates and deletes.
Draft Orderread_draft_ordersdraft_orders/create, draft_orders/update, draft_orders/delete
Draft Order Line Itemread_draft_ordersSent within Draft Order model
Orderread_ordersorders/create, orders/updated, orders/delete
Dutyread_ordersSent within Order Line Item model
Fulfillment Eventread_ordersfulfillment_events/create, fulfillment_events/delete
Fulfillmentread_ordersfulfillments/create, fulfillments/update
Fulfillment Line Itemread_ordersSent within Fulfillment model
Order Adjustmentread_ordersSent within Refund model
Order Line Itemread_ordersSent within Order model
Order Riskread_orderssynced only
Order Transactionread_ordersorder_transactions/create
Refund Dutyread_ordersSent within Refund model
Refund Line Itemread_ordersSent within Refund model
Shipping Lineread_ordersSent within Order model
Tender Transactionread_orderstender_transactions/create
Eventread_content, read_products, read_price_rules, read_orderssynced only
Fileread_files, read_themessynced only
This model tracks files uploaded by the merchant in the Files section of the Shopify Admin. Shopify doesn't expose webhooks for the File resource, so files are only updated in Gadget on sync. Files can be accessed via the read_files or read_themes scopes.
Fulfillment Orderread_assigned_fulfillment_orders, read_merchant_managed_fulfillment_orders, read_third_party_fulfillment_ordersfulfillment_orders/order_routing_complete, fulfillment_orders/fulfillment_request_submitted, fulfillment_orders/fulfillment_request_accepted, fulfillment_orders/fulfillment_request_rejected, fulfillment_orders/placed_on_hold, fulfillment_orders/cancellation_request_accepted, fulfillment_orders/cancellation_request_rejected, fulfillment_orders/cancelled, fulfillment_orders/fulfillment_service_failed_to_complete
Fulfillment Order Line Itemread_assigned_fulfillment_orders, read_merchant_managed_fulfillment_orders, read_third_party_fulfillment_orderssynced only
Fulfillment Serviceread_fulfillmentssynced only
GDPR RequestAlways availablesynced only
This model tracks incoming GDPR webhook requests from Shopify to delete merchant or customer data. These GDPR webhooks are required to be supported by Public applications for the Shopify app store, which you can read more about in the Shopify Docs. This model doesn't correspond to an API endpoint within Shopify.
Inventory Itemread_inventoryinventory_items/create, inventory_items/update, inventory_items/delete
Inventory Items are synced using the REST API endpont for Inventory Levels for each Location to get a list of inventory levels, and then using the REST API endpoint to get a list of inventory items for each level.
Inventory Levelread_inventoryinventory_levels/connect, inventory_levels/update, inventory_levels/disconnect
Inventory Levels are synced using the REST API endpoint for Inventory Levels at a Location.
Locationread_locationslocations/create, locations/update, locations/delete, locations/activate, locations/deactivate
Marketread_marketsmarkets/create, markets/update, markets/delete
Market Regionread_marketsmarkets/create, markets/update, markets/delete
Market Web Presenceread_marketsmarkets/create, markets/update, markets/delete
Pageread_contentsynced only
Shopify pages support Metafields but do not provide a REST or GraphQL mechanism for syncing them. Gadget does not support Metafields on Shopify Page models.
Payoutread_shopify_payments_payoutssynced only
Price Ruleread_price_rulessynced only
Productread_productsproducts/create, products/update, products/delete
Product Imageread_productsSent within Product model
Product Optionread_productsSent within Product model
Product Variantread_productsSent within Product model
Redirectread_contentsynced only
Script Tagread_script_tagssynced only
ShopAlways availableshop/update, app/uninstalled
Selling Plan Groupread_productsselling_plan_groups/create, selling_plan_groups/update, selling_plan_groups/delete
Selling Plan Group Productread_productsSent within Selling Plan Group model
Selling Plan Group Product Variantread_productsSent within Selling Plan Group model
Selling Planread_productsSent within Selling Plan Group model
Subscription Contractread_own_subscription_contractssubscription_contracts/create, subscription_contracts/update
Subscription Billing Attemptread_own_subscription_contractssubscription_billing_attempts/success, subscription_billing_attempts/failure, subscription_billing_attempts/challenged
Subscription Lineread_own_subscription_contractssynced only
Subscription Manual Discountread_own_subscription_contractssynced only


The sync between Shopify and my Gadget app failed.

Do you have any custom Code Effects running on the create Action on the Shopify Sync model? A custom Code Effect with errors will prevent the sync from completing successfully. Try removing or editing the code effect(s) and running a manual sync.

Code Effects aren't updating my records in Shopify.

You need to explicitly call out to the Shopify client in your Code Effect to manipulate data in Shopify. See Accessing the Shopify API.

Webhooks aren't being registered with Shopify.

If you see that some webhooks aren't being registered even after clicking Register Webhooks on the Installs page, it is possible that your app has not requested access to Protected Customer Data. See here for more details.

Frequently asked questions 

How do I make calls back to Gadget from a Shopify storefront?

You can call your Gadget app's API client in the storefront's Shopify theme. To do so, you can use the Direct Script Tag option in the Installation section of your app's API Reference.

How do I make an embedded Shopify app using Gadget?

Gadget supports being used as a backend for an app embedded in the Shopify Admin. You can read the docs for setting up the Gadget app client and Provider and go through the tutorial that walks you through setting up an embedded app with the Shopify CLI and Gadget.

When does Gadget automatically sync my Shopify Connection?

Gadget will fully sync your Shopify Connection on demand when you request a sync from the Connections view. Gadget will also automatically fetch all recent data from Shopify in the background in a scheduled daily sync. To see when your app was last synced with Shopify, you can visit the Connections view or view the Shopify Sync dataviewer.