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 Collection Events topic can be used with actions and triggers to replicate collection-related classic webhook subscriptions. Collection Events require the read_products access scope. Each accordion entry shows the equivalent Events subscription and the field-level differences between the classic webhook payload and the GraphQL response.

collections/create

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

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

  • body_html becomes descriptionHtml.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • published_at and published_scope don't map to scalar Collection fields. Use publication-related GraphQL Admin API fields when you need publication state.
  • sort_order becomes sortOrder.
  • template_suffix becomes templateSuffix.
collections/update

Maps to a Collection update subscription. No triggers are set, so any change to the collection fires a delivery, matching the behavior of the classic webhook. To narrow delivery volume, add triggers such as collection.title, collection.handle, collection.products, collection.sortOrder, or collection.templateSuffix.

Use collection.products when you want deliveries for product membership changes in a collection.

collections/delete

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

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

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 classic 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 InventoryItem Events topic can be used with actions and triggers to replicate inventory item-related classic webhook subscriptions. InventoryItem Events require the read_inventory access scope.

inventory_items/create

Maps directly to an InventoryItem create subscription. No triggers are needed.

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

  • cost becomes unitCost { amount currencyCode }.
  • country_code_of_origin becomes countryCodeOfOrigin.
  • harmonized_system_code becomes harmonizedSystemCode.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • province_code_of_origin becomes provinceCodeOfOrigin.
  • tracked_editable becomes trackedEditable.
inventory_items/update

Maps to an InventoryItem update subscription. No triggers are set, so any change to the inventory item fires a delivery, matching the behavior of the classic webhook. To narrow delivery volume, add triggers such as inventoryItem.sku, inventoryItem.tracked, inventoryItem.requiresShipping, inventoryItem.unitCost, or inventoryItem.measurement.weight.

inventory_items/delete

Maps directly to an InventoryItem delete subscription. No triggers or query are needed.

  • data.inventoryItem is null. The inventory item is deleted before the query runs.
  • id in the webhook payload is available as query_variables.inventoryItemId.

Anchor to [object Object]InventoryShipment

The InventoryShipment Events topic can be used with actions, triggers, and query_filter to replicate shipment lifecycle and tracking-related classic webhook subscriptions. InventoryShipment Events require the read_inventory_shipments access scope.

inventory_shipments/create

Maps directly to an InventoryShipment create subscription. No triggers are needed.

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

  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • tracking_company, tracking_number, and tracking_url are nested under tracking as company, trackingNumber, and trackingUrl.
  • Shipment status values use uppercase enum values, such as DRAFT, IN_TRANSIT, and RECEIVED.
inventory_shipments/mark_in_transit

Maps to an InventoryShipment update subscription with inventoryShipment.status as the trigger. Use a query_filter to receive only shipments that move to IN_TRANSIT.

inventory_shipments/update_tracking

Maps to an InventoryShipment update subscription with tracking triggers. Use the parent inventoryShipment.tracking trigger when you want any tracking change, or choose narrower triggers such as inventoryShipment.tracking.trackingNumber and inventoryShipment.tracking.trackingUrl.

inventory_shipments/delete

Maps directly to an InventoryShipment delete subscription. No triggers or query are needed.

  • data.inventoryShipment is null. The shipment is deleted before the query runs.
  • id in the webhook payload is available as query_variables.inventoryShipmentId.
inventory_shipments/add_items

Coming soon

No direct Events equivalent. This item-level shipment topic will be available in a future release.

inventory_shipments/remove_items

Coming soon

No direct Events equivalent. This item-level shipment topic will be available in a future release.

inventory_shipments/update_item_quantities

Coming soon

No direct Events equivalent. This item-level shipment topic will be available in a future release.

inventory_shipments/receive_items

Coming soon

No direct Events equivalent. This item-level shipment topic will be available in a future release.

The Location Events topic can be used with actions, triggers, and query_filter to replicate location-related classic webhook subscriptions. Location Events require the read_locations access scope. Each accordion entry shows the equivalent Events subscription and the field-level differences between the classic webhook payload and the GraphQL response.

locations/create

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

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

  • active becomes isActive.
  • address1, address2, city, country_code, province_code, and zip are nested under address.
  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • localized_country_name and localized_province_name don't map to scalar Location fields. Use address.countryCode and address.provinceCode when you need location geography.
locations/update

Maps to a Location update subscription. No triggers are set, so any change to the location fires a delivery, matching the behavior of the classic webhook. To narrow delivery volume, add triggers such as location.address, location.fulfillsOnlineOrders, location.isActive, or location.shipsInventory.

locations/activate

Maps to a Location update subscription with location.isActive as the trigger. Use a query_filter to receive only activations.

locations/deactivate

Maps to a Location update subscription with location.isActive and location.deactivatedAt as triggers. Use a query_filter to receive only deactivations.

locations/delete

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

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

The Order Events topic can be used with actions, triggers, and query_filter to replicate order-related classic webhook subscriptions. Order Events require one of the read_orders, read_marketplace_orders, read_buyer_membership_orders, or read_quick_sale access scopes. Each accordion entry shows the equivalent Events subscription and the field-level differences between the classic webhook payload and the GraphQL response.

orders/create

Maps directly to an Order create subscription. No triggers are needed.

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

  • id is a GID string instead of an integer. admin_graphql_api_id is the same value.
  • financial_status becomes displayFinancialStatus.
  • fulfillment_status becomes displayFulfillmentStatus.
  • Price fields are returned as money sets, such as totalPriceSet { shopMoney { amount currencyCode } }.
  • Line item data is available through GraphQL Admin API connections, such as lineItems.
orders/updated

Maps to an Order update subscription. No triggers are set, so any change to the order fires a delivery, matching the behavior of the classic webhook. To narrow delivery volume, add triggers such as order.email, order.note, order.tags, order.shippingLines, or order.totalPriceSet.

orders/cancelled

Maps to an Order update subscription with order.cancellation as the trigger. Include cancelReason, cancelledAt, and cancellation in the query when you need cancellation details.

orders/fulfilled

Maps to an Order update subscription with order.displayFulfillmentStatus as the trigger. Use a query_filter to receive only fulfilled orders.

orders/partially_fulfilled

Maps to an Order update subscription with order.displayFulfillmentStatus as the trigger. Use a query_filter to receive only partially fulfilled orders.

orders/paid

Maps to an Order update subscription with order.totalReceivedSet as the trigger. Use a query_filter to receive only paid orders.

orders/delete

Maps directly to an Order delete subscription. No triggers or query are needed.

  • data.order is null. The order is deleted before the query runs.
  • id in the webhook payload is available as query_variables.orderId.
orders/edited

No direct field-level Events equivalent. Use the orders/updated migration pattern and choose the specific Order triggers that correspond to the fields your app needs to replicate.

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 classic 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?