---
title: Events delivery structure
description: >-
  The structure of each delivery: JSON payload fields, HTTP headers, large
  payloads, and how to shape data with a custom query.
source_url:
  html: 'https://shopify.dev/docs/apps/build/events/delivery-structure'
  md: 'https://shopify.dev/docs/apps/build/events/delivery-structure.md'
---

# Events delivery structure

**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.

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](https://shopify.dev/docs/api/admin-graphql/latest), 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.](https://shopify.dev/assets/assets/images/apps/webhooks-events/events-gates-data-Byvf_CdL.png)

***

## Payload

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

| Field | Description |
| - | - |
| `topic` | The resource name for this delivery (for example, `Product`). |
| `action` | The operation that occurred: `create`, `update`, or `delete`. |
| `handle` | The subscription handle from your TOML configuration. Useful for routing when you have multiple subscriptions on the same topic. |
| [`fields_changed`](#fields_changed) | An array of dot-notation paths with embedded GIDs showing exactly which fields changed and on which entities. |
| [`query_variables`](#query_variables) | The 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](#headers). These fields are set in your [subscription configuration](https://shopify.dev/docs/apps/build/events/subscribe).

## Example payload

```json
{
  "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](#custom-queries):

| Field | Description |
| - | - |
| `data` | The result of your GraphQL `query`. |
| `errors` | A GraphQL errors array, returned when your `query` fails to execute (for example, referencing a removed field or a deleted resource). |

See [Custom queries](#custom-queries) for more details on how queries change the response.

### `fields_changed`

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`

`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](https://shopify.dev/docs/api/events/unstable) 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

```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](https://shopify.dev/docs/api/events/latest/product)):

* `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`.

### Large payloads

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.

| Channel | Limit |
| - | - |
| HTTP | 5 MB |
| PubSub | 10 MB |
| EventBridge | 256 KB |

**Caution:**

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

***

## Headers

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:

* `Shopify-Hmac-Sha256`: [Verify](https://shopify.dev/docs/apps/build/events/verify-deliveries) before processing any delivery.
* `Shopify-Webhook-Id`: Use to [detect and ignore duplicate deliveries](https://shopify.dev/docs/apps/build/events/verify-deliveries#ignoring-duplicates).

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](https://shopify.dev/docs/api/events/unstable#headers) in the Events reference.

***

## Custom queries

`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

##### shopify.app.toml

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

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

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

##### {} Response

```json
{
  "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

##### shopify.app.toml

```toml
[[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
    }
  }
"""
```

##### {} Response

```json
{
  "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](#query-variables-and-bubble-up) as GraphQL variables (for example `$productId`, `$variantsId`) so the query can target the specific entities involved.

### Combining with `query_filter`

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

##### shopify.app.toml

```toml
[[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"
```

##### {} Response

```json
{
  "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](https://shopify.dev/docs/apps/build/events/delivery-filtering) for filter syntax and examples.

### Query 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](https://shopify.dev/docs/api/events).

### Validation 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.

***

## Example

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

##### shopify.app.toml

```toml
[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"
```

##### {} Response

```json
{
  "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"
  }
}
```

***

## Next steps

* [Delivery filtering](https://shopify.dev/docs/apps/build/events/delivery-filtering): Control which changes qualify and whether a delivery is sent.
* [Verify deliveries](https://shopify.dev/docs/apps/build/events/verify-deliveries): HMAC verification and deduplication.

***
