Project portfolio Browse selected work

Shopify Plus Upgrade Monthly Fee Reduction + Up to $4800 Development Fee Credit - Exclusive WesWoo Offer

Guide

Why a TOML Webhook Is Missing from GraphQL: Configuration and Delivery Checks

Published: Editorial review: 2026-10-04

An empty webhookSubscriptions result can be normal when your app manages webhooks in TOML. The query returns shop-scoped subscriptions, not the app-scoped subscriptions declared in app configuration. Verify the configuration's app version and its delivery behavior before creating a replacement subscription. 2026-10 webhookSubscriptions reference.

This guide is for developers of a Shopify CLI-managed app with an authorized development-store installation and an HTTPS receiver. References were checked on 4 October 2026, using Webhook API 2026-10. The products/update topic requires read_products; check the grant alongside the app's existing scopes. Configuration, products, and payloads below are hypothetical. The Python lint was run offline; app deployment and store-triggered delivery tests were not executed. Topic scope, Scope configuration.

Establish which subscription model you are inspecting

App-scoped TOML subscriptions use a common configuration across stores where the app is installed. Shop-scoped subscriptions are suitable when configuration must vary by store. Custom apps created directly in the Shopify admin cannot use this CLI/TOML subscription workflow. Subscription models and app conditions.

QuestionTOML app-scoped subscriptionGraphQL shop-scoped subscription
What manages it?App configuration and app versionsAdmin API for the store
Does it have a subscription ID?No; it is configuration-managedYes
Where do you inspect it?Dev Dashboard app-version configurationwebhookSubscriptions
What does an empty API result prove?It does not prove TOML configuration is missingCheck the app, store, and query scope

Do not create an extra shop-scoped copy merely to make the query return a row. That can create parallel notifications without correcting the app-version configuration.

Prepare a configuration that has an observable meaning

For a hypothetical catalog consumer, receive updates only for products whose current vendor is Acme. Retain the fields needed to identify the product and observe updates. Merge this fragment into an existing app configuration; it is not a complete app file or a complete set of App Store privacy subscriptions. The HTTPS hostname is a placeholder.

[webhooks]
api_version = "2026-10"

[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "https://app.example.com/webhooks/products-update"
filter = "vendor:Acme"
include_fields = ["id", "title", "vendor", "updated_at"]
name = "catalog-acme"

topics and uri choose the event topic and destination. filter controls eligibility, and include_fields chooses payload fields. The optional name is available in delivery headers. App-scoped payloads use [webhooks].api_version. Webhook configuration fields, Subscription options.

Three behaviors matter for this fragment:

  • It matches current payload values. The equality expression vendor:Acme is case-sensitive. A title update can still qualify while the vendor remains Acme; the filter does not mean “only when the vendor changes.” Filtering semantics.
  • The filter and field list depend on each other. When include_fields is set, every field referenced by the filter must be included. Keep vendor. Invalid fields or type mismatches can allow creation while suppressing all deliveries, whereas invalid syntax prevents subscription creation. Filter constraints.
  • A smaller payload can affect debouncing. Identical small payloads from events within a short interval can cause later deliveries to be dropped. Shopify recommends including updated_at so updates produce changing payloads. This does not promise delivery of every event. Payload modification and debouncing.

Use Webhook JSON field names. The payload uses updated_at, while the GraphQL Product field is updatedAt. The current product-update sample also contains id, title, and vendor. products/update payload reference.

Catch local mistakes before examining Shopify state

Python 3.11+ provides tomllib, which raises TOMLDecodeError for an invalid document. This lint adds checks for this example's fixed topic and filter, field dependencies, API version, and HTTPS destination. It is not a general Shopify filter parser or proof that the platform accepted the configuration. Python tomllib reference.

import tomllib
from urllib.parse import urlsplit

VERSION = "2026-10"


def lint_subscription(toml_text):
    try:
        config = tomllib.loads(toml_text)
    except tomllib.TOMLDecodeError:
        return ["INVALID_TOML"]
    webhooks = config.get("webhooks")
    if not isinstance(webhooks, dict):
        return ["MISSING_WEBHOOKS"]
    issues = []
    if webhooks.get("api_version") != VERSION:
        issues.append("VERSION_MISMATCH")
    subs = webhooks.get("subscriptions")
    if not isinstance(subs, list) or len(subs) != 1:
        return issues + ["EXPECTED_ONE_EXAMPLE_SUBSCRIPTION"]
    sub = subs[0]
    if not isinstance(sub, dict):
        return issues + ["BAD_SUBSCRIPTION"]
    if sub.get("topics") != ["products/update"]:
        issues.append("TOPIC_MISMATCH")
    if sub.get("filter") != "vendor:Acme":
        issues.append("FILTER_OUTSIDE_EXAMPLE")
    fields = sub.get("include_fields")
    if not isinstance(fields, list) or any(not isinstance(f, str) for f in fields):
        issues.append("BAD_INCLUDE_FIELDS")
    else:
        if "vendor" not in fields:
            issues.append("FILTER_FIELD_MISSING")
        if "updated_at" not in fields:
            issues.append("DEBOUNCE_RISK")
        if not {"id", "title"}.issubset(fields):
            issues.append("CONSUMER_FIELD_MISSING")
    uri = sub.get("uri")
    try:
        parsed = urlsplit(uri) if isinstance(uri, str) else None
        if parsed is None or parsed.scheme != "https" or not parsed.hostname:
            issues.append("HTTPS_URI_REQUIRED")
    except ValueError:
        issues.append("HTTPS_URI_REQUIRED")
    return issues

Save the fragment as catalog-subscription.toml, then call the function from the same directory:

from pathlib import Path
issues = lint_subscription(
    Path("catalog-subscription.toml").read_text(encoding="utf-8")
)
assert issues == [], issues

Offline cases checked the valid fragment and rejected missing vendor, missing updated_at, the camelCase spelling, an HTTP destination, a wrong version or topic, and invalid TOML. A more complex filter requires additional dependency analysis and platform testing; this function deliberately refuses to validate it as this example.

Trace the file into the app version

  1. Identify the inputs. Record the configuration filename, app identity, development store, source revision, topic, URI, filter, field list, and expected API version. Have the receiver owner confirm the HTTPS backend is deployed.
  2. Apply changes in development. shopify app dev applies configuration changes to the selected development store. Verify the app and store associated with the session, then run the positive and negative checks below. Configuration application rules.
  3. Create an unreleased candidate. In the authorized project, shopify app deploy --no-release creates an app version without releasing it to merchants. This command does not deploy a self-hosted web backend. Official deploy command and flag.
  4. Inspect the version's subscriptions. In Dev Dashboard, open the app, then Versions → target version → Configuration → Subscriptions. Check the configuration-managed app subscription against your recorded configuration. A shop-scoped API query cannot replace this evidence. App-scoped management location.
  5. Release with project authorization and inspect delivery. Record the released version and the Subscription Method shown in delivery logs. Read HTTP header names case-insensitively; compare X-Shopify-Topic, X-Shopify-API-Version, and this example's X-Shopify-Name. Connect the release record to the observed payload rather than relying on the local TOML file alone. Delivery headers.

If an older integration already registered a shop-scoped copy for the same topic and destination, inventory it and plan a controlled migration. Shopify's migration procedure includes removing the old subscriptions to avoid duplicate notifications; do not add or delete subscriptions blindly during an empty-query investigation. Migration guidance.

Prove the filter with store events, not just a synthetic POST

Use identifiable test products in the authorized development store. Record the product, before/after values, action time, and app version. Establish a working positive case for the same topic before drawing conclusions from an absent negative case.

Development-store actionExpected observationFirst investigation if it differs
Change an Acme product's titleThe event qualifies; look for its deliveryEffective version, grant, URI, and delivery logs
Change its title again without changing vendorIt still qualifiesConfusing a current-value condition with a change trigger
Change a non-Acme product's titleNo corresponding filtered delivery in the observation windowCurrent vendor, effective filter, and parallel old subscriptions
Quickly change a field outside the retained field listCompare payloads and updated_atIdentical projected payloads and debouncing

CLI webhook trigger sends a synthetic payload to a specified endpoint. It is useful for receiver testing, but does not prove that a merchant event qualified through this subscription's filter. Use store-generated positive and negative events for that question. Official subscription testing instructions.

Acceptance requires an inspectable subscription in the intended app version, traceable positive and negative observations, the expected payload version and fields, and a clear next owner for configuration, access, backend, or payload-policy failures. Restore app-version configuration and backend deployment separately if rollback is needed. Reconcile synchronization gaps through the existing recovery process. For receiver authentication, delivery identity, and dead-letter handling, continue with Webhook replay safety and recovery.

Browse the WESWOO project portfolio