Shopify Webhook Filters: Syntax, Traps and Events

Shopify webhook filters are search-style expressions on a subscription, set with filter in shopify.app.toml or the webhookSubscriptionCreate mutation. Shopify checks the expression against the payload’s current values and drops the delivery if it is false. They cut traffic, but they do not detect changes, and a wrong field name silently stops every delivery.

Filters arrived in API version 2024-07. Everything below was written against Admin API 2026-10.

How do you set a filter in shopify.app.toml?

Add it to the subscription, next to include_fields if you also trim the payload.

[webhooks]
api_version = "2026-10"

[[webhooks.subscriptions]]
name = "price-sync"
topics = ["products/update"]
uri = "https://your-app.example.com/webhooks/price-sync"
include_fields = ["id", "product_type", "updated_at", "variants.id", "variants.price"]
filter = "product_type:Music OR product_type:Movies"

shopify app deploy releases it. Shopify’s subscription guide recommends these app-specific subscriptions unless topics, URIs or filters have to vary between shops.

product_type appears because every field the filter references must also be in include_fields. updated_at appears because Shopify drops a delivery whose trimmed payload matches the previous one within a short window. Shopify’s own example is ["id", "line_items.title"] on orders/updated, where consecutive price changes get debounced because neither field moved.

The metaobject topics are the one case where a filter is compulsory. metaobjects/create, /update and /delete need type:{type}, listed explicitly as in type:lookbook OR type:store_location, because type:* is not supported.

What syntax do Shopify webhook filters accept?

The documented pieces are AND, OR, parentheses, : for equality, comparisons such as :< and :>=, a trailing * for prefixes, and dots for nesting. Shopify’s example reads status:active AND (product_type:Music OR product_type:Movies) AND variants.price:>=100. The page gives no complete operator list, so stay inside those forms.

It looks like Admin search syntax and behaves differently in ways that fail quietly. Search is case-insensitive and fuzzy on tokenised fields. A filter is case-sensitive and : means equals. So product_type:music can find products in an Admin query and match nothing as a filter. An unknown field in search returns everything. In a filter it sends nothing.

Arrays match if any one object qualifies, so line_items.properties.name:_your_custom_property passes when a single line item carries that property.

Tags are odd. They are arrays in the data model but a single string in the products/update and orders/updated payloads, and the docs say to treat each tag as a keyword in that string. They do not say whether sale also matches pre-sale. Try it before you depend on it.

Why does a filtered webhook still fire on a title edit?

A filter has no before-state. variants.price:>=10.00 means “some variant is at least 10.00 now”, not “a price changed”, and the docs say outright that it fires on any product update, including a title edit. Your handler still has to work out what moved.

export function priceWrites(payload, stored) {
  const updatedAt = Date.parse(payload.updated_at);
  const last = stored.get(payload.id);

  // Shopify does not guarantee delivery order, so ignore older snapshots.
  if (last && updatedAt <= last.updatedAt) return [];

  const writes = payload.variants.filter(
    (variant) => last?.prices.get(variant.id) !== variant.price,
  );

  stored.set(payload.id, {
    updatedAt,
    prices: new Map(payload.variants.map((v) => [v.id, v.price])),
  });

  return writes;
}

stored is a Map here so the function can run as it stands. In production it is your database, and this runs in the worker after HMAC verification and deduplication on the webhook id. The updated_at check comes from Shopify’s best practices, which state that ordering within a topic is not guaranteed. It goes through Date.parse because payload timestamps carry UTC offsets, so comparing them as strings can give the wrong order.

Shopify now has a better answer on the topics it covers. Next Gen Events went generally available with API version 2026-10 on 1 October 2026, for 18 topics including Product, Order, Customer and Metaobject. You subscribe to triggers such as product.variants.price to pick which changes matter, and the delivery carries fields_changed, so the diff above stops being your job. Classic webhooks are not going away, and Shopify says to keep using them where Events has no support.

Which changes does a filter hide from you?

Exits. This follows from the current-values rule, though the webhook docs do not spell it out. The Events migration guide says it of its own filter: “A query filter can’t detect a transition by itself.”

Subscribe to products/update with status:active and archive a product. Its status is now archived, the expression is false, and no delivery goes out. Your search index keeps showing the product. A cut from 12.00 to 8.00 under variants.price:>=10.00 vanishes the same way, as does a product moved out of the product_type you filter on.

If leaving a state matters, keep that field out of the filter. List it in include_fields and make the decision in your handler. Then run the periodic job Shopify recommends anyway, because the same page tells you not to rely on receiving webhooks at all.

Why did my filter stop all deliveries?

Probably an invalid field or a type mismatch. In both cases the subscription is created and all deliveries are suppressed, with no error at any point. Bad syntax is the kinder failure, because the subscription is never created at all.

Filters read the webhook payload, which is the REST-shaped resource. product_type is a field there. The GraphQL name productType is the mistake we would look for first, and a filter field missing from include_fields second. The docs state that rule without saying what breaking it does, so check it rather than assume an error will tell you.

Do not lean on the CLI to catch any of this. shopify app webhook trigger sends a sample whose payload is always the same, to an address you pass it, and Shopify’s advice for end-to-end testing is to perform the action in Shopify. On a dev store, make one change that should pass the filter and one that should not, then confirm both in your logs.

Can a webhook filter match a metafield?

Yes, with caveats, and only on a shop-specific subscription. metafieldNamespaces is not available in TOML, so this goes through the Admin API.

mutation CreateFilteredWebhook(
  $topic: WebhookSubscriptionTopic!
  $webhookSubscription: WebhookSubscriptionInput!
) {
  webhookSubscriptionCreate(
    topic: $topic
    webhookSubscription: $webhookSubscription
  ) {
    webhookSubscription {
      id
      filter
      includeFields
      metafieldNamespaces
      uri
    }
    userErrors {
      field
      message
    }
  }
}
{
  "topic": "PRODUCTS_UPDATE",
  "webhookSubscription": {
    "uri": "https://your-app.example.com/webhooks/sync-flag",
    "filter": "metafields.namespace:sync AND metafields.key:enabled AND metafields.value:true",
    "includeFields": ["id", "updated_at", "metafields"],
    "metafieldNamespaces": ["sync"]
  }
}

The namespace is deliberately a dedicated one. In an October 2025 forum thread, a filter like this stopped firing once other custom metafields sat alongside the flag. Shopify staff reproduced it, called it expected for now and “not ideal”, and suggested checking the value server-side for complex cases. The reporter got it working by moving the flag to its own namespace. Staff also said includeFields needs metafields for these filters. That is forum evidence rather than documentation, so retest it on your version.

If the app already has an app-specific subscription to the topic, Shopify warns about conflicts and duplicate notifications when moving between the two kinds. Remove the old one first.

What did we test?

We ran the handler under node:test on Node 24.12, covering a first delivery, a title edit, a price change and a late snapshot with a different UTC offset. The GraphQL mutation validated against the 2026-10 schema, and the TOML parsed. We have not deployed these subscriptions, run these filters against a live store or tried Events, so the behaviour described above comes from Shopify’s docs and the linked thread.

What we would do

For new work on a topic Events supports, start with Events, since triggers solve the title-edit problem at the source. Stay on classic webhooks for the other topics and for integrations that already work.

There, use filters where the value cannot leave the set you care about: a line item property on a new order, a vendor you never rename. Always include updated_at when you trim. Keep the diff in the handler even with a filter on, because the filter only cuts volume.

Skip filters on any state a record can move out of, and skip them entirely on a low-traffic topic, where a handler that discards irrelevant deliveries is easier to debug than a delivery that never arrived.

If a noisy webhook sync is costing you queue time or missing changes, that is the kind of problem our Shopify development work covers.

Need this built properly?

Get in touch