Skip to main content

Migrate from webhooks

Events uses the same delivery infrastructure as classic webhooks, but gives you more control over what fires and what data you receive. Migrating means replacing your [[webhooks.subscriptions]] blocks with [[events.subscription]] blocks and mapping your webhook topics to Events equivalents.

The most significant change is the payload shape: Events payloads are GraphQL-shaped rather than REST-shaped, so field names and nested structures follow GraphQL Admin API conventions instead of REST conventions. See Events and webhooks for a full comparison.

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.


Events require Shopify CLI version 3.92 or higher.

Run shopify version to check your version, and follow the upgrade instructions if needed.


Anchor to Step 1: Map your webhook topics to EventsStep 1: Map your webhook topics to Events

For each classic webhook subscription block, migration results in an equivalent Events subscription block. Use the sections below to find the matching Events topic and action for each webhook topic string.

As you create Events equivalents for your classic webhooks, remember:

  • api_version is set at the [events] level and applies to all Events subscriptions.
  • Each subscription requires a unique handle.
  • Each topic section below shows the Events equivalent configuration, any required triggers or query_filter, and field differences between the webhook and Events payloads.
  • Not all webhook topics are currently supported by Events. You should continue to use classic webhooks in production. You can test Events migration using supported topics, and continue to use Events and classic webhook subscriptions side by side until Events supports more topics.

You can recreate webhook topics that represent business operations (for example, orders/fulfilled, orders/cancelled, customers/redact) with a combination of triggers and query_filter using the update action.

Events and classic webhooks can coexist in the same shopify.app.toml. This example shows a webhook subscription for an unsupported topic running alongside an Events subscription for a supported one. You can migrate topic by topic without removing existing webhook coverage.

The Customer Events topic can be used with actions and triggers to replicate customer-related classic webhook subscriptions. Each accordion entry shows the equivalent Events subscription and the field-level differences between the REST webhook payload and the GraphQL response.

customers/create

Maps directly to a Customer create subscription. No triggers are needed.

All REST snake_case field names use camelCase in Events. Additional field differences:

  • addresses is a connection (addresses.nodes).
  • currency has no Events equivalent. The webhook derived it from store defaults.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • last_order_id and last_order_name merge into lastOrder { id name }.
  • state uses uppercase enum values.
  • tags returns an array instead of a comma-separated string.
  • total_spent becomes amountSpent { amount currencyCode }.
customers/update

Maps to a Customer update subscription. Without triggers, this fires on any change to the customer record.

All REST snake_case field names use camelCase in Events. Additional field differences:

  • addresses is a connection (addresses.nodes).
  • currency has no Events equivalent. The webhook derived it from store defaults.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • last_order_id and last_order_name merge into lastOrder { id name }.
  • state uses uppercase enum values.
  • tags returns an array instead of a comma-separated string.
  • total_spent becomes amountSpent { amount currencyCode }.
customers/disable

Fires when a customer account is disabled. Maps to a Customer update subscription with triggers = ["customer.state"] and query_filter = "customer.state:'DISABLED'". Without the filter, the subscription also fires when an account is enabled.

All REST snake_case field names use camelCase in Events. Additional field differences:

  • addresses is a connection (addresses.nodes).
  • currency has no Events equivalent. The webhook derived it from store defaults.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • last_order_id and last_order_name merge into lastOrder { id name }.
  • state is always "DISABLED".
  • tags returns an array instead of a comma-separated string.
  • total_spent becomes amountSpent { amount currencyCode }.
customers/enable

Fires when a customer account is enabled. Maps to a Customer update subscription with triggers = ["customer.state"] and query_filter = "customer.state:'ENABLED'". Without the filter, the subscription also fires when an account is disabled.

All REST snake_case field names use camelCase in Events. Additional field differences:

  • addresses is a connection (addresses.nodes).
  • currency has no Events equivalent. The webhook derived it from store defaults.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • last_order_id and last_order_name merge into lastOrder { id name }.
  • state is always "ENABLED".
  • tags returns an array instead of a comma-separated string.
  • total_spent becomes amountSpent { amount currencyCode }.
customer.tags_added

Fires when tags are added to a customer. Maps to a Customer update subscription with triggers = ["customer.tags"].

Unlike customers/disable and customers/enable, there's no query_filter expression that can distinguish a tag addition from a tag removal. query_filter evaluates the resource's current state, not the delta. Both customer.tags_added and customer.tags_removed map to the same Events subscription. Your handler receives the full current tag set and must determine what changed by comparing against its own stored state.

The webhook payload is already GraphQL-flavored. Field differences:

  • occurredAt approximates updatedAt on Customer. They match for tag events, but updatedAt advances on any customer change.
  • tags in Events is the full current tag set, not just the added tags.
customer.tags_removed

Fires when tags are removed from a customer. Maps to the same Customer update subscription as customer.tags_added. There's no query_filter expression that can distinguish a tag removal from a tag addition, because query_filter evaluates the resource's current state, not the delta. Your handler receives the remaining tag set and must determine what was removed by comparing against its own stored state.

The webhook payload is already GraphQL-flavored. Field differences:

  • occurredAt approximates updatedAt on Customer. They match for tag events, but updatedAt advances on any customer change.
  • tags in Events is the remaining tag set after removal, not the removed tags.
customers/purchasing_summary

Fires when purchase totals update. Maps to a Customer update subscription with triggers covering the purchasing fields your handler needs.

The webhook payload is already GraphQL-flavored. Field differences:

  • lastOrderId becomes lastOrder { id }.
  • numberOfOrders is a string (UnsignedInt64), not an integer.
  • occurredAt approximates Customer.updatedAt. They match for purchasing events, but updatedAt advances on any customer change.
customers/delete

Maps directly to a Customer delete subscription. No triggers or query are needed.

  • admin_graphql_api_id and id are available as query_variables.customerId.
  • data.customer is null. The customer is deleted before the query runs.
  • tax_exemptions has no Events equivalent.
customers/data_request

Coming soon

No direct Events equivalent. This GDPR topic will be available in a future release.

customers/marketing_consent/update

Coming soon

No direct Events equivalent. This GDPR topic will be available in a future release.

customers/redact

Coming soon

No direct Events equivalent. This GDPR topic will be available in a future release.

customers/merge

Coming soon

No direct Events equivalent. This topic will be available in a future release.

customer.joined_segment

Coming soon

No direct Events equivalent. This topic will be available in a future release.

customer.left_segment

Coming soon

No direct Events equivalent. This topic will be available in a future release.


The Product Events topic can be used with actions and triggers to replicate product-related classic webhook subscriptions. Each accordion entry shows the equivalent Events subscription and the field-level differences between the REST webhook payload and the GraphQL response.

products/create

Maps directly to a Product create subscription. No triggers are needed.

All REST snake_case field names use camelCase in Events. The following fields also rename, restructure, or have no equivalent:

  • body_html becomes descriptionHtml.
  • category becomes category { id name }.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • image (featured image) becomes featuredImage.
  • image_id on variants becomes image { id url altText }.
  • images and variants are connections (images.nodes, variants.nodes) instead of flat arrays. Use variants.nodes[*].id in place of variant_gids.
  • inventory_item_id on variants becomes inventoryItem { id }.
  • inventory_policy on variants and status use uppercase enum values (DENY/CONTINUE and ACTIVE/DRAFT/ARCHIVED).
  • option1/option2/option3 on variants become selectedOptions { name value }.
  • tags returns an array instead of a comma-separated string.
  • has_variants_that_requires_components, old_inventory_quantity on variants, and published_scope have no Events equivalent.
products/update

Maps to a Product update subscription. No triggers are set, so any change to the product fires a delivery, matching the behavior of the classic webhook. To narrow delivery volume after migration, see Delivery filtering.

products/delete

Maps directly to a Product delete subscription. No triggers or query are needed.

  • data.product is null. The product is deleted before the query runs.
  • id in the webhook payload is available as query_variables.productId.

Anchor to Step 2: Match your delivery structureStep 2: Match your delivery structure

If your webhook used include_fields, modify your Events query to match those fields and reproduce the same payload. Field names change from REST snake_case to GraphQL camelCase, and nested fields become proper GraphQL selections.


Anchor to Step 3: Match how you filter deliveriesStep 3: Match how you filter deliveries

If your webhook used filter, express the same condition as query_filter on your Events subscription. Field names become camelCase with the topic name as a prefix, and enum values become uppercase to match GraphQL Admin API conventions.

Some things to consider as you translate your filter:

  • id:* drops.
  • product_type becomes product.productType: camelCase, prefixed with the topic name.
  • status:active becomes product.status:'ACTIVE': enum values are uppercase in the GraphQL Admin API.
  • variants.price becomes product.variants.price: camelCase, prefixed with the topic name.

Anchor to Step 4: Update your handlersStep 4: Update your handlers

Events payloads are GraphQL-shaped, so your handlers need to read from a different structure than classic webhook payloads.

  • The payload is wrapped in a data object keyed by the topic name (for example, data.customer or data.product).
  • All field names are camelCase instead of snake_case.
  • Some fields are restructured or renamed. Check the field differences listed in each topic section above.
  • fields_changed lists which fields triggered the delivery. Use it to avoid re-processing unchanged data.
  • For delete subscriptions, data is absent. Use query_variables to identify what was deleted.

See the Events reference for the full payload structure and available fields.


Anchor to Step 5: Test your new subscriptionsStep 5: Test your new subscriptions

With both subscription blocks active, make changes on your test store and verify that your new endpoint receives deliveries with the expected shape. Confirm that fields_changed, query_variables, and data all look correct before removing the old subscription.


Anchor to Step 6: Remove the migrated webhooksStep 6: Remove the migrated webhooks

After your Events subscriptions are working and your handlers are updated, remove each migrated [[webhooks.subscriptions]] block from your TOML and redeploy. Your old endpoints stop receiving deliveries for those topics.


  • Narrow delivery volume: Add triggers to your update subscriptions so deliveries fire only when specific fields change. See Delivery filtering.
  • Expand your query: Now that you control the payload shape, request only the fields your handler actually uses, or add fields the webhook payload never included. See Delivery structure.
  • Consolidate subscriptions: You can collapse multiple webhook topic strings for the same resource into one Events subscription with multiple actions and a single handler.
  • Events reference: Full topic and trigger path reference.

Was this page helpful?