--- title: Create an Events subscription description: >- Set up your first Events subscription in shopify.app.toml and process deliveries in your app. source_url: html: 'https://shopify.dev/docs/apps/build/events/get-started' md: 'https://shopify.dev/docs/apps/build/events/get-started.md' --- # Create an Events subscription Suppose you're building an app that keeps a merchant's external product catalog in sync with Shopify. The external catalog already exists. Your app's job is to reflect changes as they happen. When a product is created in Shopify, your app needs to add it to the external catalog. When it's deleted, your app needs to remove it. When a variant's price or compare-at price changes, your app needs to patch the corresponding record. In this tutorial, you'll set up Events subscriptions for all three actions. **Developer preview:** Events is in developer preview on the [`unstable`](https://shopify.dev/docs/api/usage/versioning#making-requests-to-an-api-version) API version, available today for a [subset of topics](https://shopify.dev/docs/api/events). Use it for early testing ahead of a stable release and broader topic coverage. For topics not yet supported, use webhooks alongside Events in the same `shopify.app.toml`. As Events expands topic coverage, it will become the primary subscription mechanism. ## What you'll learn In this tutorial, you'll learn how to do the following tasks: * Write Events subscriptions for create, delete, and update actions. * Use `triggers` and a custom `query` to receive only the data you need. * Handle deliveries in route handlers, and use `fields_changed` and `query_variables` to process those deliveries. ## Requirements [Shopify CLI version 3.92 or higher](https://shopify.dev/docs/api/shopify-cli#upgrade) Events require Shopify CLI version 3.92 or higher. Run `shopify version` to check, and see [upgrade instructions](https://shopify.dev/docs/api/shopify-cli#upgrade) if needed. [Scaffold an app](https://shopify.dev/docs/apps/build/scaffold-app) Scaffold an app that uses the [React Router template](https://github.com/Shopify/shopify-app-template-react-router). ## Project [View on GitHub](https://github.com/Shopify/shopify-app-template-react-router/blob/events-subscribe-example-https) ## Subscribe to product Events Configure subscriptions in `shopify.app.toml`, define a route to receive deliveries, and process the payload in your handler. You'll start with `create` and `delete`, then add an `update` subscription that narrows what's delivered with `triggers`, `query`, and `query_filter`. ### Configure top-level properties Before you add subscriptions, set the access scope your topic requires and pin the Events API version. #### Update your access scopes Events topics require scopes. Because you're subscribing to Product topic changes, include the `read_products` scope in the configuration file. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` #### Set the version Add an `[events]` block and pin `api_version` to `unstable`. Events is only available on the `unstable` API version while it's in developer preview. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` ### Subscribe to product creates and deletes Create subscriptions for `create` and `delete` actions on the Product topic, then define a single route handler that receives both. The handler uses `authenticate.webhook` to verify the request and read `shop`, `topic`, and `payload` from the delivery. #### Configure a `create` subscription Add an `[[events.subscription]]` block. The `handle` identifies which subscription triggered a delivery. Set `topic` to the resource name, `actions` to the operations to listen for, and `uri` to the route that will receive deliveries. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` #### Configure a `delete` subscription Add a second `[[events.subscription]]` block for the `Product` topic with `actions = ["delete"]` using the same route. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` #### Define a route Use `authenticate.webhook` from your app's `shopify.server` module. Shopify's library validates the request and gives you `shop`, `topic`, and `payload`. Log one short line and the full `payload` so you can inspect it in the terminal where `shopify app dev` is running. All subscriptions in this tutorial will reuse this same endpoint. If you're not using the React Router template, you'll need to verify HMAC signatures yourself. See [Verify Events deliveries](https://shopify.dev/docs/apps/build/events/verify-deliveries) for the raw verification approach. ## /app/routes/events.app.products.tsx ```tsx import {authenticate} from '../shopify.server'; export const action = async ({request}: {request: Request}) => { const {shop, topic, payload} = await authenticate.webhook(request); console.log(`Received ${topic} Event for ${shop}`); console.log(JSON.stringify(payload, null, 2)); return new Response(null, {status: 200}); }; ``` ### Subscribe to product updates Create another subscription to receive updates for specific field changes, not just create and delete actions. You'll add `triggers` to narrow deliveries to variant price and compare-at price changes, a `query` to fetch exactly the data your sync needs, and a `query_filter` to suppress deliveries for inactive products. #### Define the subscription Add a new `[[events.subscription]]` block for the `Product` topic with `actions = ["update"]`. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` #### Add triggers Add `triggers` to narrow deliveries to variant-level changes your sync logic actually needs. Without triggers, every update to the product fires a delivery. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` #### Add a query Add a `query` to shape the payload. When a trigger fires, Shopify executes this query and includes the result in the `data` field of the delivery. Without a query, the delivery has no `data` field. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` #### Add a query filter Add `query_filter` to suppress deliveries for products that shouldn't be synced to the external catalog. The filter evaluates after the query runs, so you can filter on any field returned by your query. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` ### Process update deliveries Update deliveries with a configured `query` include `fields_changed`, `query_variables`, and `data`. Each field serves a different purpose in your handler. #### Read `fields_changed` to identify what triggered the delivery `fields_changed` is an array of field paths, each embedding the GID of the entity that changed. Use it to know which specific field and variant caused this delivery. ## /response.jsonc ```jsonc { "topic": "Product", "action": "update", "handle": "update_product_events", "data": { "productVariant": { "id": "gid://shopify/ProductVariant/789", "price": "24.99", "compareAtPrice": "29.99" }, "product": { "id": "gid://shopify/Product/123", "title": "Example product", "status": "ACTIVE" } }, "fields_changed": [ "product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/789'].price" ], "query_variables": { "productId": "gid://shopify/Product/123", "variantsId": "gid://shopify/ProductVariant/789" } } ``` #### Use `query_variables` to access entity IDs `query_variables` gives you the entity IDs directly. No need to parse them from the query response or from `fields_changed` paths. ## /response.jsonc ```jsonc { "topic": "Product", "action": "update", "handle": "update_product_events", "data": { "productVariant": { "id": "gid://shopify/ProductVariant/789", "price": "24.99", "compareAtPrice": "29.99" }, "product": { "id": "gid://shopify/Product/123", "title": "Example product", "status": "ACTIVE" } }, "fields_changed": [ "product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/789'].price" ], "query_variables": { "productId": "gid://shopify/Product/123", "variantsId": "gid://shopify/ProductVariant/789" } } ``` #### Act on the query data The `data` field contains the response from your subscription's `query`. Use it to access the fields your app needs without making a follow-up API call. **Info:** Queries executed by subscriptions don't count toward your rate limits but have a 250 point complexity limit. ## /response.jsonc ```jsonc { "topic": "Product", "action": "update", "handle": "update_product_events", "data": { "productVariant": { "id": "gid://shopify/ProductVariant/789", "price": "24.99", "compareAtPrice": "29.99" }, "product": { "id": "gid://shopify/Product/123", "title": "Example product", "status": "ACTIVE" } }, "fields_changed": [ "product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/789'].price" ], "query_variables": { "productId": "gid://shopify/Product/123", "variantsId": "gid://shopify/ProductVariant/789" } } ``` ### Test your subscriptions Run your app on a dev store and confirm each subscription delivers as expected. #### Set up testing on a dev store Save your TOML file and run `shopify app dev`. If `app dev` is already running, the subscription is automatically created or updated. #### Confirm `create` and `delete` behavior Navigate to your test shop and create a new product. The payload should print to your CLI. Delete a product. Your delete endpoint should receive a delivery. #### Confirm `update` behavior Change the price of a product variant on an active product. The payload should print to your CLI, including `fields_changed`, `query_variables`, and `data`. Update the price on an inactive product, or update the title on an active product. In both cases, you don't receive a delivery. ### Deploy your app When you're ready to release your subscriptions to production, navigate to your app directory and run the following command: ## Terminal ```terminal shopify app deploy ``` Optionally, you can provide a name or message for the version using the `--version` and `--message` flags. Releasing an app version replaces the current active version that's served to stores that have your app installed. It might take several minutes for app users to be upgraded to the new version. **Tip:** If you want to create a version, but avoid releasing it to users, then run the `deploy` command with a `--no-release` flag. You can release the unreleased app version using Shopify CLI's [`release`](https://shopify.dev/docs/api/shopify-cli/app/app-release) command, or through the Dev Dashboard. ## /shopify.app.toml ```toml name = "Events subscription" application_url = "https://example.com/" embedded = true [build] automatically_update_urls_on_dev = true [access_scopes] scopes = "read_products" [events] api_version = "unstable" [[events.subscription]] handle = "create_product_events" topic = "Product" actions = ["create"] uri = "/events/app/products" [[events.subscription]] handle = "delete_product_events" topic = "Product" actions = ["delete"] uri = "/events/app/products" [[events.subscription]] handle = "update_product_events" topic = "Product" actions = ["update"] triggers = [ "product.variants.price", "product.variants.compareAtPrice" ] uri = "/events/app/products" query = """ query price_change($productId: ID!, $variantsId: ID!){ productVariant(id: $variantsId) { id price compareAtPrice } product(id: $productId) { id title status } } """ query_filter = "product.status:'ACTIVE'" ``` ## /app/routes/events.app.products.tsx ```tsx import {authenticate} from '../shopify.server'; export const action = async ({request}: {request: Request}) => { const {shop, topic, payload} = await authenticate.webhook(request); console.log(`Received ${topic} Event for ${shop}`); console.log(JSON.stringify(payload, null, 2)); return new Response(null, {status: 200}); }; ``` ## /response.jsonc ```jsonc { "topic": "Product", "action": "update", "handle": "update_product_events", "data": { "productVariant": { "id": "gid://shopify/ProductVariant/789", "price": "24.99", "compareAtPrice": "29.99" }, "product": { "id": "gid://shopify/Product/123", "title": "Example product", "status": "ACTIVE" } }, "fields_changed": [ "product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/789'].price" ], "query_variables": { "productId": "gid://shopify/Product/123", "variantsId": "gid://shopify/ProductVariant/789" } } ``` ## Next steps [About Events\ \ ](https://shopify.dev/docs/apps/build/events) [Explore supported topics, delivery structure, filtering options, and the full Events reference.](https://shopify.dev/docs/apps/build/events) [Migrate from webhooks\ \ ](https://shopify.dev/docs/apps/build/events/migrate-from-webhooks) [Convert an existing webhook subscription to Events and start using triggers, queries, and delivery filtering.](https://shopify.dev/docs/apps/build/events/migrate-from-webhooks)