Skip to main content

Migrate from webhooks

Events uses the same delivery infrastructure as classic webhooks, with field-level triggers and data selected by a GraphQL query. Use this guide to translate the REST-shaped fields your webhook handler reads into GraphQL Admin API selections, then design an Events subscription around the changes your app needs.

Use the topic mapping below to find the Events resource for your classic webhook. The starter queries illustrate GraphQL field selections; the topic references provide supported triggers, query variables, and subscription examples. These queries are a starting point for field mapping. They don't configure a subscription or reproduce the conditions that caused the classic webhook to fire.


  • Use Shopify CLI version 3.92 or higher. Run shopify version to check your version, and follow the upgrade instructions if needed.
  • Identify the classic webhook topics your app uses, the changes that cause them to fire, and the fields your handler reads.
  • Check the Events reference for supported topics, triggers, query variables, and required access scopes.

Anchor to Step 1: Translate your webhook fieldsStep 1: Translate your webhook fields

Find your Events topic below, and use its starter query or linked subscription example to select the fields your handler needs. REST field names generally change from snake_case to camelCase, but some fields have different structures or no GraphQL equivalent.

Complexity points estimate the work required by a query based on its selected fields and connection sizes. By default, scalar and enum fields cost zero points, object fields cost one point, and connection costs depend on first and last. Some fields have custom costs, so use the GraphQL Admin API cost calculation rules to estimate complexity and validate your subscription query to confirm it meets the limit.

Events subscription queries have a complexity limit of 100 points. The broad starter queries can exceed that limit, especially when they select large connections or nested objects. Optimize your query before using it in a subscription. Connection arguments such as first: 250 limit the number of records returned. A page size isn't a complexity budget or a guarantee of a complete result.

For delete actions, there's no starter query: the resource no longer exists. Use the identifiers in query_variables to identify what Shopify deleted. Definition topics use a type or a namespace, key, and owner type instead of a queryable resource ID. See Events delivery structure.

Use the Collection reference to choose supported triggers and query variables for your app. Requires read_products access scope.

collections/create

Use this query to translate the fields from a collection creation webhook.

Field differences:

  • body_html becomes descriptionHtml.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • 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

Use the same collection field mapping for updates. Identify whether your handler needs collection details, product membership changes, or both, then choose supported triggers in the Collection reference.

collections/delete

No query is needed for a deleted Collection. Use query_variables.collectionId to identify it.

Starter query: collections/create

query collection_create_starter($collectionId: ID!) {
collection(id: $collectionId) {
id
handle
title
updatedAt
descriptionHtml
sortOrder
templateSuffix
}
}

Use the Company reference to choose supported triggers and query variables for your app. Query the company with companyId. Contact, contact role, and location changes are updates to the company.

companies/create

Use this query to translate the fields from a company creation webhook. external_id becomes externalId. Add the contact or location selections your handler needs using the reference examples.

companies/update

Use the same company field mapping for updates. Choose supported triggers for the company fields your handler needs.

companies/delete

No query is needed for a deleted Company. Use query_variables.companyId to identify it.

company_contacts/create

Contact creation is an update to the parent company. Review company.contacts.* and select the current contact fields your handler needs. Use child query variables only with triggers that expose them.

company_contacts/update

Choose supported company.contacts.* triggers for contact changes. Select the current contact fields your handler needs. Use child query variables only with triggers that expose them.

company_contacts/delete

Contact removal is an update to the parent company. Use fields_changed to identify the removed contact instead of querying it. Use child query variables only with triggers that expose them.

company_contact_roles/assign

Review company.contacts.roleAssignments and company.locations.roleAssignments for role assignment changes. Query the company and add the contact or location selections your handler needs. Use child query variables only with triggers that expose them.

company_contact_roles/revoke

Review company.contacts.roleAssignments and company.locations.roleAssignments for role assignment changes. Query the company and add the contact or location selections your handler needs. Use child query variables only with triggers that expose them.

company_locations/create

Location creation is an update to the parent company. Review company.locations.* and select the current location fields your handler needs. Use child query variables only with triggers that expose them.

company_locations/update

Choose supported company.locations.* triggers for location changes. Select the current location fields your handler needs. Use child query variables only with triggers that expose them.

company_locations/delete

Location removal is an update to the parent company. Use fields_changed to identify the removed location instead of querying it. Use child query variables only with triggers that expose them.

Starter query: companies/create

query companies_create_starter($companyId: ID!) {
company(id: $companyId) {
id
name
externalId
note
}
}

Use the Customer reference to choose supported triggers and query variables for your app.

For customers_email_marketing_consent/update and customers_marketing_consent/update, review the customer.defaultEmailAddress.* and customer.defaultPhoneNumber.smsMarketingConsent.* triggers. Select the current consent fields from Customer and use fields_changed to identify the changed fields. A consent change doesn't provide the merge operation details from customers/merge.

customers/create

Use this query to translate the fields from a customer creation webhook.

Field differences:

  • currency has no Events equivalent. The webhook derives it from store defaults.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • Address customer_id, country_name, and default fields have no Events equivalents.
  • Address id values are GID strings instead of integers.
  • state uses uppercase enum values.
customers/update

Use this query to translate customer fields, then choose triggers for the changes your handler needs.

Field differences:

  • currency has no Events equivalent. The webhook derives it from store defaults.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • Address customer_id, country_name, and default fields have no Events equivalents.
  • Address id values are GID strings instead of integers.
  • state uses uppercase enum values.
customers/disable

This webhook represents a customer account becoming disabled. The query returns the current customer state; selecting state doesn't restrict delivery to that transition. Review state triggers and filtering when you design your subscription.

Field differences:

  • currency has no Events equivalent. The webhook derives it from store defaults.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • Address customer_id, country_name, and default fields have no Events equivalents.
  • Address id values are GID strings instead of integers.
  • state uses uppercase enum values. The query can return any current state.
customers/enable

This webhook represents a customer account becoming enabled. The query returns the current customer state; selecting state doesn't restrict delivery to that transition. Review state triggers and filtering when you design your subscription.

Field differences:

  • currency has no Events equivalent. The webhook derives it from store defaults.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • Address customer_id, country_name, and default fields have no Events equivalents.
  • Address id values are GID strings instead of integers.
  • state uses uppercase enum values. The query can return any current state.
customer.tags_added

Use this query to retrieve the current tag set. Determine what changed by comparing it with your stored state. A query filter evaluates the current data and can't distinguish a tag addition from a removal.

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

Use this query to retrieve the remaining tag set. Determine what changed by comparing it with your stored state. A query filter evaluates the current data and can't distinguish a tag removal from an addition.

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

Use this query to translate purchasing summary fields. Check the Customer reference for triggers that cover the purchasing changes your handler needs.

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

No query is needed for a deleted Customer. Use query_variables.customerId to identify it.

Starter query: customers/create

query customer_create_starter($customerId: ID!) {
customer(id: $customerId) {
id
createdAt
updatedAt
firstName
lastName
state
note
verifiedEmail
multipassIdentifier
taxExempt
email
phone
taxExemptions
defaultAddress {
id
firstName
lastName
company
address1
address2
city
province
country
zip
phone
name
provinceCode
countryCode
}
addresses {
id
firstName
lastName
company
address1
address2
city
province
country
zip
phone
name
provinceCode
countryCode
}
}
}

Use the FulfillmentOrder reference to choose supported triggers and query variables for your app. Query the parent with fulfillmentOrderId; a classic fulfillment ID isn't a fulfillment order ID.

fulfillments/create

Fulfillment creation is an update to the parent fulfillment order. Review fulfillmentOrder.fulfillments.* and add the fulfillment selections your handler needs.

fulfillments/update

Review fulfillmentOrder.fulfillments.status and fulfillmentOrder.fulfillments.trackingInfo.* for fulfillment updates. Add the status and tracking fields your handler reads.

fulfillment_orders/cancelled

Review fulfillmentOrder.status and evaluate the resulting state. Cancellation is an update, not deletion of the fulfillment order.

fulfillment_orders/cancellation_request_submitted

Review fulfillmentOrder.requestStatus and fulfillmentOrder.merchantRequests for cancellation requests. Query the resulting state.

fulfillment_orders/cancellation_request_accepted

Review fulfillmentOrder.requestStatus and fulfillmentOrder.status for accepted cancellation requests.

fulfillment_orders/cancellation_request_rejected

Review fulfillmentOrder.requestStatus for rejected cancellation requests. Query the resulting state.

fulfillment_orders/fulfillment_request_submitted

Review fulfillmentOrder.requestStatus and fulfillmentOrder.merchantRequests for fulfillment requests. Add the request fields your handler needs.

fulfillment_orders/fulfillment_request_accepted

Review fulfillmentOrder.requestStatus for accepted fulfillment requests and evaluate the resulting state.

fulfillment_orders/fulfillment_request_rejected

Review fulfillmentOrder.requestStatus for rejected fulfillment requests and evaluate the resulting state.

fulfillment_orders/placed_on_hold

Review fulfillmentOrder.fulfillmentHolds and fulfillmentOrder.status. Add current hold selections to the query and evaluate the resulting state.

fulfillment_orders/hold_released

Review fulfillmentOrder.fulfillmentHolds and fulfillmentOrder.status for hold changes. Compare the resulting state with the state your handler tracks.

fulfillment_holds/added

Review fulfillmentOrder.fulfillmentHolds and add the current hold selections your handler needs.

fulfillment_holds/released

Review fulfillmentOrder.fulfillmentHolds and query the parent fulfillment order.

fulfillment_orders/rescheduled

Review fulfillmentOrder.fulfillAt and fulfillmentOrder.fulfillBy for scheduling changes. fulfill_at and fulfill_by become fulfillAt and fulfillBy.

Starter query: fulfillments/create

query fulfillments_create_starter($fulfillmentOrderId: ID!) {
fulfillmentOrder(id: $fulfillmentOrderId) {
id
status
requestStatus
fulfillAt
fulfillBy
}
}

Use the InventoryItem reference to choose supported triggers and query variables for your app. Requires read_inventory access scope.

For inventory_levels/connect, inventory_levels/update, and inventory_levels/disconnect, review the inventoryItem.inventoryLevel.*, inventoryItem.inventoryLevel.isActive, and quantity triggers. These are updates to InventoryItem, not creation or deletion of the item itself. Use inventoryLevelId only with triggers that expose it. For disconnects, subscribe to inventoryItem.inventoryLevel.isActive, query the inventory level with inventoryLevelId, and check whether isActive is false.

inventory_items/create

Use this query to translate the fields from an inventory item creation webhook.

Field differences:

  • cost becomes unitCost { amount currencyCode }.
  • country_code_of_origin becomes countryCodeOfOrigin.
  • country_harmonized_system_codes becomes countryHarmonizedSystemCodes.nodes.
  • harmonized_system_code becomes harmonizedSystemCode.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • province_code_of_origin becomes provinceCodeOfOrigin.
  • weight_value and weight_unit are nested under measurement.weight as value and unit.
inventory_items/update

Use the same inventory item field mapping for updates. Choose triggers for the item fields your handler needs, and remove unused fields and connections from the query.

inventory_items/delete

No query is needed for a deleted InventoryItem. Use query_variables.inventoryItemId to identify it.

Starter query: inventory_items/create

query inventory_item_create_starter($inventoryItemId: ID!) {
inventoryItem(id: $inventoryItemId) {
id
sku
createdAt
updatedAt
tracked
requiresShipping
countryCodeOfOrigin
provinceCodeOfOrigin
harmonizedSystemCode
countryHarmonizedSystemCodes(first: 250) {
nodes {
countryCode
harmonizedSystemCode
}
}
measurement {
weight {
value
unit
}
}
unitCost {
amount
currencyCode
}
}
}

Anchor to [object Object]InventoryShipment

Use the InventoryShipment reference to choose supported triggers and query variables for your app. Requires read_inventory_shipments access scope.

inventory_shipments/create

Use this query to translate shipment fields. The line item connection is a starting point; reconcile additional pages separately if your app needs every item.

Field differences:

  • happened_at and inventory_transfer_id have no Events equivalents.
  • line_items becomes the lineItems.nodes connection.
  • tracking fields use camelCase, including arrivesAt, trackingNumber, and trackingUrl.
inventory_shipments/mark_in_transit

This webhook represents a shipment moving to IN_TRANSIT. The query returns the current status. Review status triggers and filtering to decide which transitions your app needs to process.

inventory_shipments/update_tracking

Use this query to translate tracking fields. Choose supported tracking triggers based on the changes your handler needs.

inventory_shipments/delete

No query is needed for a deleted InventoryShipment. Use query_variables.inventoryShipmentId to identify it.

Starter query: inventory_shipments/create

query inventory_shipment_create_starter($inventoryShipmentId: ID!) {
inventoryShipment(id: $inventoryShipmentId) {
id
status
tracking {
arrivesAt
company
trackingNumber
trackingUrl
}
lineItems(first: 250) {
nodes {
id
quantity
}
}
}
}

Anchor to [object Object]InventoryTransfer

Use the InventoryTransfer reference to choose supported triggers and query variables for your app. Query the transfer with inventoryTransferId. Paginate the line items when your handler needs the complete transfer.

inventory_transfers/updated

Use this query to select the current transfer fields your handler reads. Review status, destination, line item quantity, and tag triggers for the changes your handler needs.

inventory_transfers/add_items

Review inventoryTransfer.lineItems.* for line item changes. The query returns current state. Compare it with stored state to identify added items.

inventory_transfers/remove_items

Review inventoryTransfer.lineItems.* for line item changes. Compare the queried current state with stored state to identify removed items. Don't rely on fields_changed to provide item IDs or an added/removed list.

inventory_transfers/update_item_quantities

Review inventoryTransfer.lineItems.totalQuantity and select current line item quantities. The query returns totalQuantity for each line item. Compare with stored state if your handler needs the webhook's old_quantity and new_quantity difference.

inventory_transfers/cancel

Review inventoryTransfer.status and evaluate the resulting state for cancellation. Cancellation is an update, not deletion of the transfer.

inventory_transfers/complete

Review inventoryTransfer.status and evaluate the resulting state for completion.

inventory_transfers/ready_to_ship

Review inventoryTransfer.status and evaluate the resulting state when the transfer becomes ready to ship.

Starter query: inventory_transfers/updated

query inventory_transfers_updated_starter($inventoryTransferId: ID!) {
inventoryTransfer(id: $inventoryTransferId) {
id
name
status
tags
lineItems(first: 10) {
nodes {
id
totalQuantity
}
}
}
}

Use the Location reference to choose supported triggers and query variables for your app. Requires read_locations access scope.

locations/create

Use this query to translate the fields from a location creation webhook.

Field differences:

  • active becomes isActive.
  • Address fields are nested under address.
  • country and province become address.country and address.province. Their code fields become address.countryCode and address.provinceCode.
  • country_name is also represented by address.country.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • legacy has no Events equivalent.
locations/update

Use the same location field mapping for updates. Choose supported triggers for the location details your handler needs.

locations/activate

This webhook represents a location becoming active. The query returns isActive; choose triggers and filtering based on the state transitions your app needs.

locations/deactivate

This webhook represents a location becoming inactive. The query returns isActive; choose triggers and filtering based on the state transitions your app needs.

locations/delete

No query is needed for a deleted Location. Use query_variables.locationId to identify it.

Starter query: locations/create

query location_create_starter($locationId: ID!) {
location(id: $locationId) {
id
name
createdAt
updatedAt
isActive
address {
address1
address2
city
zip
province
country
countryCode
provinceCode
phone
}
}
}

Anchor to [object Object]MetafieldDefinition

Use the MetafieldDefinition reference to choose supported triggers and query variables for your app.

Look up a definition by namespace, key, and owner type. metafieldDefinitionId isn't available to payload queries. Events supplies metafieldDefinitionOwnerType as a String, so you can't pass it directly to the GraphQL MetafieldOwnerType enum.

metafield_definitions/create

Use this query to translate the fields from a metafield definition creation webhook. This example selects product definitions. Adapt the fixed ownerType for the definitions your app handles and keep selections specific to that owner type.

metafield_definitions/update

Use the same definition lookup for updates. Choose supported triggers for the definition fields your handler needs, such as metafieldDefinition.name, metafieldDefinition.description, or metafieldDefinition.validations.

metafield_definitions/delete

No query is needed for a deleted MetafieldDefinition. Use query_variables.metafieldDefinitionNamespace, query_variables.metafieldDefinitionKey, and query_variables.metafieldDefinitionOwnerType to identify it.

Starter query: metafield_definitions/create

query metafield_definitions_create_starter(
$metafieldDefinitionNamespace: String!
$metafieldDefinitionKey: String!
) {
metafieldDefinition(identifier: {
namespace: $metafieldDefinitionNamespace
key: $metafieldDefinitionKey
ownerType: PRODUCT
}) {
id
namespace
key
name
description
ownerType
type { name }
}
}

Use the Metaobject reference to choose supported triggers and query variables for your app. Query an entry with metaobjectId.

metaobjects/create

Use this query to translate the fields from a metaobject creation webhook. display_name becomes displayName. The custom field example uses title; replace it with a key from your definition.

metaobjects/update

Use the same metaobject field mapping for updates. Update triggers require a metaobject type, such as metaobject(type: 'lookbook').displayName. For custom field changes, select a field key in both the trigger and query.

metaobjects/delete

No query is needed for a deleted Metaobject. Use query_variables.metaobjectId to identify it.

Starter query: metaobjects/create

query metaobjects_create_starter($metaobjectId: ID!) {
metaobject(id: $metaobjectId) {
id
type
handle
displayName
field(key: "title") { key value }
}
}

Use the Order reference to choose supported triggers and query variables for your app. Requires one of the read_orders, read_marketplace_orders, read_buyer_membership_orders, or read_quick_sale access scopes.

orders/create

Use this query to translate order fields. Its broad field selection and nested connections need optimization before you use it in an Events subscription.

Field differences:

  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • financial_status becomes displayFinancialStatus.
  • fulfillment_status becomes displayFulfillmentStatus.
  • Price fields are returned as money sets, such as totalPriceSet { shopMoney { amount currencyCode } }.
  • browser_ip becomes clientIp, buyer_accepts_marketing becomes customerAcceptsMarketing, and note_attributes becomes customAttributes.
  • order_status_url becomes statusPageUrl, referring_site becomes referrerUrl, and source_url becomes registeredSourceUrl.
  • Line items, discount applications, shipping lines, and returns are connections with nodes instead of flat arrays.
  • client_details, device_id, landing_site_ref, reference, and token have no direct Events equivalents.
orders/updated

Use the same order field mapping for updates. Choose supported triggers for the order changes your handler needs, then reduce the query to the relevant fields.

orders/cancelled

This webhook represents an order cancellation. Use cancelReason and cancelledAt for cancellation details, and check the Order reference for the corresponding change triggers.

orders/fulfilled

This webhook represents an order becoming fulfilled. The query returns displayFulfillmentStatus; selecting it alone doesn't restrict deliveries to fulfilled orders. Review fulfillment triggers and filtering.

orders/partially_fulfilled

This webhook represents an order becoming partially fulfilled. The query returns displayFulfillmentStatus; selecting it alone doesn't restrict deliveries to partially fulfilled orders. Review fulfillment triggers and filtering.

orders/paid

This webhook represents an order becoming paid. The query returns displayFinancialStatus; review the Order reference and your payment workflow to choose triggers and filtering.

orders/delete

No query is needed for a deleted Order. Use query_variables.orderId to identify it.

orders/edited

There isn't a direct field-level Events equivalent. Start with the orders/updated query and identify the supported triggers that cover the order edits your app needs.

Starter query: orders/create

query order_create_starter($orderId: ID!) {
order(id: $orderId) {
id
app { id }
clientIp
customerAcceptsMarketing
cancelReason
cancelledAt
cartToken
checkoutToken
closedAt
confirmationNumber
confirmed
email
createdAt
currencyCode
currentShippingPriceSet { ...MoneyBagFields }
currentSubtotalPriceSet { ...MoneyBagFields }
currentTotalAdditionalFeesSet { ...MoneyBagFields }
currentTotalDiscountsSet { ...MoneyBagFields }
currentTotalDutiesSet { ...MoneyBagFields }
currentTotalPriceSet { ...MoneyBagFields }
currentTotalTaxSet { ...MoneyBagFields }
customerLocale
discountCodes
dutiesIncluded
estimatedTaxes
name
displayFinancialStatus
displayFulfillmentStatus
landingPageUrl
landingPageDisplayText
physicalLocation { id }
retailLocation { id }
merchantBusinessEntity { id }
merchantOfRecordApp { id }
note
customAttributes { key value }
number
statusPageUrl
originalTotalAdditionalFeesSet { ...MoneyBagFields }
originalTotalDutiesSet { ...MoneyBagFields }
paymentGatewayNames
phone
poNumber
presentmentCurrencyCode
processedAt
referrerUrl
sourceIdentifier
sourceName
registeredSourceUrl
subtotalPriceSet { ...MoneyBagFields }
tags
taxExempt
taxLines { ...TaxLineFields }
taxesIncluded
test
totalCashRoundingAdjustment {
paymentSet { ...MoneyBagFields }
refundSet { ...MoneyBagFields }
}
totalDiscountsSet { ...MoneyBagFields }
totalOutstandingSet { ...MoneyBagFields }
totalPriceSet { ...MoneyBagFields }
totalShippingPriceSet { ...MoneyBagFields }
totalTaxSet { ...MoneyBagFields }
totalTipReceived { amount currencyCode }
totalWeight
updatedAt
staffMember { id }
billingAddress { ...MailingAddressFields }
customer { ...CustomerFields }
discountApplications(first: 250) {
nodes { ...DiscountApplicationFields }
}
fulfillments { id }
lineItems(first: 250) {
nodes {
id
currentQuantity
fulfillableQuantity
fulfillmentService { handle }
fulfillmentStatus
isGiftCard
weight { value unit }
name
originalUnitPrice
originalUnitPriceSet { ...MoneyBagFields }
product { id }
customAttributes { key value }
quantity
requiresShipping
lineItemGroup { id }
sku
taxable
title
totalDiscount
totalDiscountSet { ...MoneyBagFields }
variant { id }
variantTitle
vendor
staffMember { id }
taxLines { ...TaxLineFields }
duties {
id
countryCodeOfOrigin
harmonizedSystemCode
price { ...MoneyBagFields }
taxLines { ...TaxLineFields }
}
discountAllocations {
allocatedAmount { amount currencyCode }
allocatedAmountSet { ...MoneyBagFields }
discountApplication { ...DiscountApplicationFields }
}
}
}
paymentTerms { id }
refunds { id }
shippingAddress { ...MailingAddressFields }
shippingLines(first: 250, includeRemovals: true) {
nodes {
id
carrierIdentifier
code
currentDiscountedPriceSet { ...MoneyBagFields }
discountedPrice { amount currencyCode }
discountedPriceSet { ...MoneyBagFields }
isRemoved
phone
originalPrice { amount currencyCode }
originalPriceSet { ...MoneyBagFields }
source
title
taxLines { ...TaxLineFields }
discountAllocations {
allocatedAmount { amount currencyCode }
allocatedAmountSet { ...MoneyBagFields }
discountApplication { ...DiscountApplicationFields }
}
}
}
returns(first: 250) { nodes { id } }
}
}

fragment MoneyBagFields on MoneyBag {
shopMoney { amount currencyCode }
presentmentMoney { amount currencyCode }
}

fragment MailingAddressFields on MailingAddress {
firstName
lastName
company
address1
address2
city
province
country
zip
phone
name
provinceCode
countryCode
latitude
longitude
}

fragment CustomerFields on Customer {
id
createdAt
updatedAt
firstName
lastName
state
note
verifiedEmail
multipassIdentifier
taxExempt
email
phone
taxExemptions
defaultAddress { ...MailingAddressFields }
}

fragment TaxLineFields on TaxLine {
title
rate
price
priceSet { ...MoneyBagFields }
channelLiable
}

fragment DiscountApplicationFields on DiscountApplication {
__typename
index
allocationMethod
targetSelection
targetType
value {
... on MoneyV2 { amount currencyCode }
... on PricingPercentageValue { percentage }
}
... on AutomaticDiscountApplication { title }
... on DiscountCodeApplication { code }
... on ManualDiscountApplication { title }
... on ScriptDiscountApplication { title }
}

Use the Product reference to choose supported triggers and query variables for your app.

variants/in_stock and variants/out_of_stock describe availability transitions. Product doesn't expose an inventory quantity trigger. Review the InventoryItem inventory-level triggers for quantity changes.

products/create

Use this query to translate product fields, including the variants, images, and media embedded in classic webhooks. The connections are a starting point for field mapping and need optimization before use in Events.

Field differences:

  • body_html becomes bodyHtml.
  • category becomes category { id name fullName }.
  • has_variants_that_requires_components becomes hasVariantsThatRequiresComponents.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.
  • image (featured image) becomes featuredImage.
  • image_id on variants becomes image { id url altText }.
  • The classic webhook embeds images, media, and variants as flat arrays. The query selects them through connections with nodes. Variant id values correspond to variant_gids; connection pagination can omit variants.
  • 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.
  • old_inventory_quantity on variants and published_scope have no Events equivalents.
products/update

Use the same product field mapping for updates. Identify whether your handler needs product, variant, or media changes. When a supported trigger provides a child ID, query the changed child directly instead of reloading the connection. See Optimizing your subscriptions.

products/delete

No query is needed for a deleted Product. Use query_variables.productId to identify it.

Starter query: products/create

query product_create_starter($productId: ID!) {
product(id: $productId) {
id
title
handle
status
vendor
productType
bodyHtml
createdAt
updatedAt
publishedAt
templateSuffix
tags
hasVariantsThatRequiresComponents
options {
id
name
position
values
}
featuredImage {
id
altText
url
width
height
}
category {
id
name
fullName
}
variants(first: 250) {
nodes {
id
title
price
compareAtPrice
sku
barcode
position
createdAt
updatedAt
taxable
inventoryPolicy
inventoryQuantity
product { id }
image { id url altText }
inventoryItem { id }
selectedOptions { name value }
}
}
media(first: 250) {
nodes {
... on Media {
id
alt
mediaContentType
status
preview {
status
image {
id
altText
url
width
height
}
}
}
... on MediaImage {
image {
id
altText
url
width
height
}
}
... on Video {
duration
sources {
fileSize
format
height
mimeType
url
width
}
}
... on Model3d {
sources {
filesize
format
mimeType
url
}
}
... on ExternalVideo {
embedUrl
host
originUrl
}
}
}
images(first: 250) {
nodes {
id
altText
url
width
height
}
}
}
}

Use the Refund reference to choose supported actions and query variables for your app. Refunds also support update actions for supported refund line item, adjustment, and transaction changes. They don't support a delete action.

refunds/create

Use this query to translate the fields from a refund creation webhook. Subscribe to the create action and query with refundId.

Field differences:

  • created_at becomes createdAt.
  • order_id becomes order { id }.
  • The webhook's admin_graphql_api_id becomes id. Numeric REST IDs don't have an Events equivalent.

Query the parent refund and select the child data your handler needs. Don't assume child IDs in fields_changed are available as payload query variables.

Starter query: refunds/create

query refunds_create_starter($refundId: ID!) {
refund(id: $refundId) {
id
createdAt
note
order { id }
totalRefundedSet {
shopMoney { amount currencyCode }
presentmentMoney { amount currencyCode }
}
}
}

Use the Return reference to choose supported triggers and query variables for your app. Query the return with returnId. Returns can start in either REQUESTED or already-approved OPEN status. Later approval, decline, cancellation, closing, and reopening use supported update triggers and the resulting state.

Events queries return the state when the query runs, which can differ from the state when the event occurred. A requested return can be approved before its create query runs, and an already-approved return can close or cancel before that query runs. Don't use the queried status to determine which operation created the return.

returnLineItems contains interface values. Use an inline fragment on ReturnLineItem when selecting concrete fields such as restockingFee. The reference includes examples for these fields.

returns/request

Keep the returns/request webhook when your app needs a notification for each return request. The Events create action includes both requested returns and already-approved returns created through returnCreate. Checking the queried status for REQUESTED can miss a request that Shopify has already approved.

For current-state synchronization, subscribe to the create action and the update action with the return.status trigger. Use the starter query to retrieve the return's current state, not to reconstruct a request notification.

The webhook's admin_graphql_api_id becomes id. Select order { id } for the related order's GraphQL ID.

returns/approve

Keep the returns/approve webhook when your app needs approval notifications, including for returns created already approved through returnCreate. Checking an Events create delivery's queried status for OPEN doesn't reliably identify those operations. A return can reach OPEN after creation or leave it before the query runs.

To refresh approval data with Events, subscribe to the create action and the update action with the return.requestApprovedAt trigger. Query status and requestApprovedAt as current values. A create delivery and a later approval update can both show the same approval, so don't run an approval operation for each delivery. A status change alone doesn't necessarily indicate approval either. For example, reopening a return also changes its status to OPEN.

returns/decline

Review return.decline and return.status for decline changes. Add the decline selections your handler needs and evaluate the resulting state.

returns/cancel

Review return.status and evaluate the resulting state for cancellation. Cancellation is an update, not deletion of the return.

returns/close

Review return.closedAt and return.status for closing changes. Use the queried status and timestamp to evaluate the resulting state.

returns/reopen

Review return.status and return.closedAt for reopening changes.

returns/process

Review return.returnLineItems.refundedQuantity and return.returnLineItems.unprocessedQuantity for processing changes. Add the current line item quantities your handler needs to the query.

returns/update

Review supported return field and line item quantity triggers. Add the fields your handler reads to the query and use fields_changed to identify changed fields.

reverse_fulfillment_orders/dispose

Continue using the reverse_fulfillment_orders/dispose webhook for disposal notifications. Events doesn't provide an equivalent disposition trigger in 2026-10. A disposal can occur without changing the reverse fulfillment order's status or removing a line item, so return.reverseFulfillmentOrders.* doesn't reliably capture these operations.

Starter query: returns/request

query returns_request_starter($returnId: ID!) {
return(id: $returnId) {
id
name
status
closedAt
order { id }
}
}

Anchor to Step 2: Identify what should trigger your subscriptionStep 2: Identify what should trigger your subscription

A query selects data. It doesn't determine when an Events subscription fires. For each classic webhook you want to migrate, identify the operation or field change your app reacts to and find the corresponding Events topic, action, and supported triggers.

  • For create, query the new resource using its ID.
  • For update, choose triggers for the specific changes your app needs. Every subscription whose actions include update requires triggers.
  • For delete, identify the deleted resource from query_variables instead of querying it.

Check the variables available for every action and trigger you select. A query can require a child ID only when every trigger in that subscription provides it. Split changes into separate subscriptions when they need different IDs or query roots.

For business operations such as an order becoming paid or a customer account becoming enabled, identify both the change and the resulting state that matter to your app. Use triggers to detect changes and query_filter to check the current query result. A query filter can't detect a transition by itself. If your app tracks a set of resources, then include the changes that remove resources from that set as well as those that add them.

See Delivery filtering for trigger and query filter behavior.


Anchor to Step 3: Optimize your queryStep 3: Optimize your query

Use Optimizing your subscriptions for the detailed workflow and worked examples. Apply it to each starter query before configuring your replacement subscription:

  1. Keep only the fields your handler and query_filter need. If your classic webhook uses include_fields, then use that list as your starting point.
  2. Follow the optimization guide to choose targeted queries, split subscriptions, and reconcile any data that deliveries don't include.
  3. Validate the optimized subscription to confirm that its query meets the 100-point complexity limit. Shopify checks complexity when you deploy an app version, without executing the query or waiting for an event. Use shopify app deploy --no-release to validate without releasing the version to users.

  • Subscribe to Events: Configure a subscription after you've chosen its actions, triggers, and optimized query.
  • Events delivery structure: Update your handler for GraphQL field names, query variables, and delivery metadata.
  • Troubleshoot Events: Inspect test deliveries and diagnose failures before changing your existing webhook coverage.

Was this page helpful?