---
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)
