Skip to main content

Events delivery structure

Developer preview

Events is in developer preview on the unstable API version, available today for a subset of topics. 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.

Each qualifying change sends a delivery to your uri as an HTTP POST with a JSON body and a set of headers. By default, the body includes metadata about the subscription and the change. Add a query to include custom data from the GraphQL Admin API, and combine with a query_filter to define conditions where the delivery is sent or suppressed.

Illustration of Events pipeline and how configuration results in reshaped data.

Every delivery includes a JSON body. The following fields are always present:

FieldDescription
topicThe resource name for this delivery (for example, Product).
actionThe operation that occurred: create, update, or delete.
handleThe subscription handle from your TOML configuration. Useful for routing when you have multiple subscriptions on the same topic.
fields_changedAn array of dot-notation paths with embedded GIDs showing exactly which fields changed and on which entities.
query_variablesThe entity IDs resolved for this delivery in a flat key-value format, matching the variables available to your query.

topic, action, and handle are also repeated in headers. These fields are set in your subscription configuration.

Example payload

{
"topic": "Product",
"action": "update",
"handle": "product-updates",
"fields_changed": [
"product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price"
],
"query_variables": {
"productId": "gid://shopify/Product/123",
"variantsId": "gid://shopify/ProductVariant/456"
}
}

The following fields are present only when a query is configured:

FieldDescription
dataThe result of your GraphQL query.
errorsA GraphQL errors array, returned when your query fails to execute (for example, referencing a removed field or a deleted resource).

See Custom queries for more details on how queries change the response.

Each entry is a path with GIDs embedded so you can identify exactly which entity and field changed:

fields_changed entry

product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price

Use fields_changed to branch logic when a subscription covers multiple trigger paths.

query_variables contains the same IDs in a flat shape suitable for use as GraphQL variables in your handler code. The available variables depend on the topic and triggers configured for the subscription. The Events reference lists the variables available for each trigger.

Although you can combine any collection of triggers together in an Event subscription to a particular topic, there will be cases when variables available to some triggers in your subscription aren't available to others. This mismatch can result in errors when attempting to reference unavailable variables in your queries.

For example, consider the following subscription:

shopify.app.toml

[events]
api_version = "unstable"

[[events.subscription]]
handle = "my_product_events"
topic = "Product"
actions = ["update"]
triggers = [
"product.variants.price",
"product.title"
]

query = """
query product_details($productId: ID!, $variantsId: ID!) {
product(id: $productId) {
id
title
status
}
productVariant(id: $variantsId) {
id
price
}
}
"""

The following variables are available to each trigger (see the product Events reference):

  • product.title: productId
  • product.variants.price: productId and variantsId

If you try to define your query in this way, the subscription is invalid and you'll receive an error stating variantsId is not available in product.title.

When the body would exceed the limit for the delivery channel, Shopify stores the full content and sends a small payload that includes only topic, action, handle, payload_url, payload_size_bytes, and expires_at.

fields_changed, query_variables, and data are present in the full content available at payload_url.

When handling overflow payloads:

  • Verify the HMAC signature on the small payload, not on the downloaded body.
  • Download the full content from payload_url before it expires.
  • Treat payload_url as a short-lived credential. It's unguessable and expires.

Your handlers must be prepared for both inline and overflow payloads. Check for the presence of payload_url before attempting to read fields_changed, query_variables, or data from the body.

ChannelLimit
HTTP5 MB
PubSub10 MB
EventBridge256 KB
Caution

Keep queries only as large as your app needs to reduce overflow risk.


Events deliveries include HTTP headers with metadata about the subscription and change. Treat header names as case-insensitive in your code, as HTTP/2 often lowercases them. For PubSub and EventBridge deliveries, these headers are included alongside the payload.

Two headers require active handling:

The topic, action, and handle values from your subscription are also present as headers, useful for routing at the HTTP layer without parsing the body.

For the complete list, see HTTP headers in the Events reference.


query is an optional GraphQL Admin API operation defined in your subscription.

When set, Shopify runs it after a qualifying change and includes the result in the delivery's data field. The query isn't limited to the subscribed topic, can reference multiple root fields in a single operation, and can include any valid fields your app's scopes allow.

Without a query, deliveries include fields_changed and query_variables but no data:

Without query

[[events.subscription]]
handle = "product-updates"

topic = "Product"
actions = ["update"]
triggers = ["product.variants.price"]

uri = "https://your-app.example.com/events"
{
"topic": "Product",
"action": "update",
"handle": "product-updates",
"fields_changed": [
"product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price"
],
"query_variables": {
"productId": "gid://shopify/Product/123",
"variantsId": "gid://shopify/ProductVariant/456"
}
}

Adding query tells Shopify to run a GraphQL Admin API operation after the change and include the result in data:

With query

[[events.subscription]]
handle = "price-sync"

topic = "Product"
actions = ["update"]
triggers = ["product.variants.price"]

uri = "https://your-app.example.com/events"

query = """
query price_change($productId: ID!, $variantsId: ID!) {
product(id: $productId) {
title
status
}
productVariant(id: $variantsId) {
id
price
sku
}
}
"""
{
"topic": "Product",
"action": "update",
"handle": "price-sync",
"data": {
"product": {
"title": "Widget",
"status": "ACTIVE"
},
"productVariant": {
"id": "gid://shopify/ProductVariant/456",
"price": "29.99",
"sku": "WIDGET-BLUE"
}
},
"fields_changed": [
"product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price"
],
"query_variables": {
"variantsId": "gid://shopify/ProductVariant/456",
"productId": "gid://shopify/Product/123"
}
}

IDs from the change bubble up as GraphQL variables (for example $productId, $variantsId) so the query can target the specific entities involved.

Add query_filter to gate whether the delivery is sent based on current values from the query result. When both are set, Shopify runs them in sequence:

  • query runs first to build data
  • query_filter evaluates against that result and suppresses the delivery if it resolves to false

If query_filter is set, query must also be configured.

With query and query_filter

[[events.subscription]]
handle = "price-sync"

topic = "Product"
actions = ["update"]
triggers = ["product.variants.price"]

uri = "https://your-app.example.com/events"

query = """
query price_change($productId: ID!, $variantsId: ID!) {
product(id: $productId) {
title
status
}
productVariant(id: $variantsId) {
id
price
}
}
"""

query_filter = "product.status:'ACTIVE' AND productVariant.price:>100"
{
"topic": "Product",
"action": "update",
"handle": "price-sync",
"data": {
"product": {
"title": "Widget",
"status": "ACTIVE"
},
"productVariant": {
"id": "gid://shopify/ProductVariant/456",
"price": "129.99"
}
},
"fields_changed": [
"product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price"
],
"query_variables": {
"variantsId": "gid://shopify/ProductVariant/456",
"productId": "gid://shopify/Product/123"
}
}

See Filter Events deliveries for filter syntax and examples.

Anchor to Query variables and bubble-upQuery variables and bubble-up

IDs flow up from the changed entity through the ownership hierarchy, so a variant change also provides $productId. Variable names follow the field path pattern in camelCase (for example $variantsId, $productId).

Available variables for your topic and triggers are listed in the Events reference.

Anchor to Validation and runtime errorsValidation and runtime errors

The query is validated when the subscription is saved and must only reference variables that your triggers can supply. At runtime, GraphQL errors from the query appear in the payload's errors field. Check errors in your handler to detect and log query failures.

Common query issues:

  • The query references a field that doesn't exist on the type.
  • A required variable isn't available. Variables like $variantsId and $productId are injected automatically, but only when the entity they reference is involved in the change.

Queries run with your app's access scopes. Fields you can't read might return null rather than an error.


The following subscription delivers only when a variant's price changes on a product that is currently active and priced above $100:

Product price change

[events]
api_version = "unstable"

[[events.subscription]]
handle = "product-price-change"
topic = "Product"
actions = ["update"]

triggers = [
"product.variants.price",
"product.variants.compareAtPrice"
]

uri = "https://your-app.example.com/events"

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' AND productVariant.price:>100"
{
"topic": "Product",
"action": "update",
"handle": "product-price-change",
"data": {
"productVariant": {
"id": "gid://shopify/ProductVariant/456",
"price": "129.99",
"compareAtPrice": "149.99"
},
"product": {
"id": "gid://shopify/Product/123",
"title": "Widget",
"status": "ACTIVE"
}
},
"fields_changed": [
"product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price"
],
"query_variables": {
"variantsId": "gid://shopify/ProductVariant/456",
"productId": "gid://shopify/Product/123"
}
}


Was this page helpful?