Working with Shopify webhooks
Shopify webhooks within model actions
Shopify webhooks within model actions in Gadget ensure your application is kept up to date with the latest Shopify store data.
When an event occurs in your Shopify store, such as the creation, update, or deletion of a product, Shopify sends a webhook to your Gadget application. This webhook contains information about the event that just occurred.
In Gadget, each Shopify model has corresponding actions (create
, update
, delete
) that are triggered by these webhooks. For instance, if Shopify sends a products/create
webhook, Gadget will run the create
action on the Product model in your app. This action uses the incoming parameters from the webhook to create a new record in your Gadget database.
To handle these webhooks:
- Gadget verifies the authenticity of the webhook to ensure it's coming from Shopify.
- It then triggers the corresponding action associated with the webhook event.
- The complete payload of the webhook is accessible within the
trigger
object passed to your action code, allowing you to use the data within your action's logic. - If an action fails, Gadget will retry it up to 10 times with increasing delays between attempts. If the action fails on the last attempt, the webhook is considered "lost", though Gadget will later attempt to automatically reconcile the missing webhook with Shopify.
Reconciliation of webhooks
As described in the above process, in the case when a webhook fails or is missed, Gadget automatically catches the missed webhook during a daily reconciliation and re-runs the corresponding action.
Effectively, this means that your data will always go back in sync with Shopify, even after an outage, because of the automated reconciliation that is built into our webhook processing.
Shopify webhooks within global actions
Shopify webhooks can trigger global actions if a Shopify connection has already been set up on a Gadget app. To run a global action when a webhook is received, add the Shopify data trigger to your global action, and configure which webhook topics to listen to. Gadget will register the webhook topics, and trigger the global action when Shopify sends webhook payloads.
Shopify data triggers on global actions are similar to Shopify data triggers on model actions, however, there is no nightly reconciliation for global action webhook triggers. If a webhook is missed, its payload will not be retrieved at a later time and the action will not be run.
Only webhook topics granted by the access scopes selected in a Shopify connection will be available as global action triggers. You can select multiple webhook topics to dispatch to the same global action as well.
If one or more webhooks have been disabled for a particular shop, those webhooks will also no longer trigger any global actions.
Adding a Shopify webhook trigger to a global action
To add a Shopify data trigger to a global action:
- Select your global action in
api/actions
- Click + in the TRIGGERS panel and select Shopify
- Select one or more webhook topics
The webhook payload can be found in the trigger
object provided by the action context parameter.
1export const run: ActionRun = async ({ trigger, logger }) => {2 // access product payload attributes at trigger.payload.id3 // access the topic at trigger.topic4 const isShopifyTrigger = trigger.type === "shopify_webhook";5 if (isShopifyTrigger) {6 logger.info(7 {8 productID: trigger.payload.id,9 title: trigger.payload.title,10 topic: trigger.topic,11 },12 "product update webhook happened"13 );14 }15};1617export const options = {18 triggers: {19 shopify: {20 webhooks: ["products/update"],21 },22 api: false,23 },24};
1export const run: ActionRun = async ({ trigger, logger }) => {2 // access product payload attributes at trigger.payload.id3 // access the topic at trigger.topic4 const isShopifyTrigger = trigger.type === "shopify_webhook";5 if (isShopifyTrigger) {6 logger.info(7 {8 productID: trigger.payload.id,9 title: trigger.payload.title,10 topic: trigger.topic,11 },12 "product update webhook happened"13 );14 }15};1617export const options = {18 triggers: {19 shopify: {20 webhooks: ["products/update"],21 },22 api: false,23 },24};
When to use webhook triggers within global actions
It's best to use webhook triggers on global actions when you want to run code that doesn't affect the state of your application or database. This is because there is no reconciliation for these triggers, so data integrity and consistency can't be guaranteed.
Below are some examples where you might use webhook triggers within global actions.
Use cases
- Real-time notifications and alerts: You can use webhooks to send notifications to your team or customers. For example, you might send a Slack message to your fulfillment team when a new order is created, or notify a customer via SMS or email when their order status changes.
- Forward data to an external service: You can forward a webhook payload to an existing service, for example pushing Shopify order data to an ERP. Depending on the type of data being forwarded, you may want the resiliency built into model action webhooks for this use case.
If the same webhook is being used to trigger both a global action and a model action, the order and timing of action execution are not guaranteed. Either action may be run first, or they may be run concurrently, so actions triggered by the same webhook should not depend on one another.
Disable webhook processing per shop
Gadget supports limiting which webhooks are processed by certain shops to reduce webhook volume. Certain Shopify shops generate a ton of webhooks. Processing and storing these webhooks can add up to cost too much, especially if you aren't making use you don't want to pay to process and consume webhooks for all models for all shops that have installed your app. For example, within your Shopify app, you may want to disable certain webhooks from processing on Shopify models for free trial users so you do not end up adding up your costs for users on your free trial.
Within Gadget, you can disable certain webhooks from processing so you have greater control over your app. Every shopifyShop
model has a disabledWebhooks
field which is a boolean value set to false by default. When the value is changed to true
this will disable webhooks within the scheduled sync from syncing data concerning the model associated with the disabledWebhooks
value changing.
For example, let's take a look below at our update action with our shopifyShop
model where within the run
function we'll set our disabledWebhooks
field to true when it's a shopifyProduct
model. What we've defined here is any reconciliation sync going forward on webhook re-registrations will disable webhook processing and won't sync data for the shopifyProduct
model. If we also head to our shopifyShop
data we'll notice within the registeredWebhooks
column that the shopifyProduct
is no longer associated with its value.
1import { applyParams, save, ActionOptions } from "gadget-server";2import { preventCrossShopDataAccess } from "gadget-server/shopify";34export const run: ActionRun = async ({5 params,6 record,7 logger,8 api,9 connections,10}) => {11 applyParams(params, record);1213 record.disabledWebhooks = { shopifyProduct: true };14 // Disabling all webhooks that sync data for the shopifyProduct model1516 await preventCrossShopDataAccess(params, record);17 await save(record);18};1920export const onSuccess: ActionOnSuccess = async ({21 params,22 record,23 logger,24 api,25 connections,26}) => {27 // Your logic goes here28};2930export const options: ActionOptions = {31 actionType: "update",32};
1import { applyParams, save, ActionOptions } from "gadget-server";2import { preventCrossShopDataAccess } from "gadget-server/shopify";34export const run: ActionRun = async ({5 params,6 record,7 logger,8 api,9 connections,10}) => {11 applyParams(params, record);1213 record.disabledWebhooks = { shopifyProduct: true };14 // Disabling all webhooks that sync data for the shopifyProduct model1516 await preventCrossShopDataAccess(params, record);17 await save(record);18};1920export const onSuccess: ActionOnSuccess = async ({21 params,22 record,23 logger,24 api,25 connections,26}) => {27 // Your logic goes here28};2930export const options: ActionOptions = {31 actionType: "update",32};
You have to explicitly set a model associated with the disabledWebhooks
field value change like the above example, it will not work if
you just flip the boolean value to true
.
Handling deletion webhooks
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.
Registering Shopify webhooks
In some cases, you may need to manually register webhooks to ensure your Shopify app receives the necessary events from a store.
You should only have to manually register webhooks on development environments. Gadget automatically updates webhook registrations during deployment, so you should not need to manually register webhooks for a production environment. If the selected access scopes have changed and are deployed to production, stores that have installed your app will need to re-authenticate to grant your app access to the new scopes, and webhook registration will be done at that time.
Here are the scenarios when you should manually register Shopify webhooks in development environments:
When editing connections: If you edit a connection to include new models, you may need to register new webhooks for those models. For example, if you add the Collection model, you should register webhooks to receive
collections/create
,collections/update
, andcollections/delete
. If you edit the connection to include additional access scopes, you will have to re-authenticate your app on your development store to grant the app access to the new scopes, and new webhooks will be automatically registered.When missing webhooks: If you notice that certain webhooks are not registered, as indicated on the Installs page of your Shopify connection, you should manually register the missing webhooks. This can happen if you are missing access scopes or topics/namespaces for your registered webhooks.
API version upgrade: Shopify releases a new API version every 3 months, and when you upgrade your Shopify Connection's API version, you should ensure that your webhooks are still correctly registered for the new version.
When accessing protected customer data: If you are using an Event subscriptions version later than
2022-07
, you will need to request access permissions in the Protected customer data access section on the API access page of your Shopify Partners app. After completing the required sections, return to your Gadget app and register the webhooks.
How to register Shopify webhooks
To register Shopify webhooks, you can:
- Navigate to the Installs page for your Connection in Gadget (Settings -> Plugins -> Shopify)
- Click the ⋮ button for any stores you have re-authenticated, and select the Register Webhooks option:
Shopify webhook status
The status and timeline of a Shopify webhook can be observed within the Queues dashboard in Gadget. Here you'll be able to view all Shopify webhooks running and observe the progress in how they're running.