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.
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 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.
Anchor to RequirementsRequirements
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_versionis 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
triggersorquery_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.
Anchor to CustomerCustomer
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:
addressesis a connection (addresses.nodes).currencyhas no Events equivalent. The webhook derived it from store defaults.idis a GID string instead of an integer.admin_graphql_api_idis the same value.last_order_idandlast_order_namemerge intolastOrder { id name }.stateuses uppercase enum values.tagsreturns an array instead of a comma-separated string.total_spentbecomesamountSpent { 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:
addressesis a connection (addresses.nodes).currencyhas no Events equivalent. The webhook derived it from store defaults.idis a GID string instead of an integer.admin_graphql_api_idis the same value.last_order_idandlast_order_namemerge intolastOrder { id name }.stateuses uppercase enum values.tagsreturns an array instead of a comma-separated string.total_spentbecomesamountSpent { 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:
addressesis a connection (addresses.nodes).currencyhas no Events equivalent. The webhook derived it from store defaults.idis a GID string instead of an integer.admin_graphql_api_idis the same value.last_order_idandlast_order_namemerge intolastOrder { id name }.stateis always"DISABLED".tagsreturns an array instead of a comma-separated string.total_spentbecomesamountSpent { 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:
addressesis a connection (addresses.nodes).currencyhas no Events equivalent. The webhook derived it from store defaults.idis a GID string instead of an integer.admin_graphql_api_idis the same value.last_order_idandlast_order_namemerge intolastOrder { id name }.stateis always"ENABLED".tagsreturns an array instead of a comma-separated string.total_spentbecomesamountSpent { amount currencyCode }.
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:
lastOrderIdbecomeslastOrder { id }.numberOfOrdersis a string (UnsignedInt64), not an integer.occurredAtapproximatesCustomer.updatedAt. They match for purchasing events, butupdatedAtadvances on any customer change.
customers/delete
Maps directly to a Customer delete subscription. No triggers or query are needed.
admin_graphql_api_idandidare available asquery_variables.customerId.data.customerisnull. The customer is deleted before the query runs.tax_exemptionshas 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.
Anchor to ProductProduct
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_htmlbecomesdescriptionHtml.categorybecomescategory { id name }.idis a GID string instead of an integer.admin_graphql_api_idis the same value.image(featured image) becomesfeaturedImage.image_idon variants becomesimage { id url altText }.imagesandvariantsare connections (images.nodes,variants.nodes) instead of flat arrays. Usevariants.nodes[*].idin place ofvariant_gids.inventory_item_idon variants becomesinventoryItem { id }.inventory_policyon variants andstatususe uppercase enum values (DENY/CONTINUEandACTIVE/DRAFT/ARCHIVED).option1/option2/option3on variants becomeselectedOptions { name value }.tagsreturns an array instead of a comma-separated string.has_variants_that_requires_components,old_inventory_quantityon variants, andpublished_scopehave 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.productisnull. The product is deleted before the query runs.idin the webhook payload is available asquery_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_typebecomesproduct.productType: camelCase, prefixed with the topic name.status:activebecomesproduct.status:'ACTIVE': enum values are uppercase in the GraphQL Admin API.variants.pricebecomesproduct.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
dataobject keyed by the topic name (for example,data.customerordata.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_changedlists which fields triggered the delivery. Use it to avoid re-processing unchanged data.- For
deletesubscriptions,datais absent. Usequery_variablesto 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.
Anchor to Next stepsNext steps
- Narrow delivery volume: Add
triggersto yourupdatesubscriptions 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
actionsand a single handler. - Events reference: Full topic and trigger path reference.