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.
| Question | TOML app-scoped subscription | GraphQL shop-scoped subscription |
|---|---|---|
| What manages it? | App configuration and app versions | Admin API for the store |
| Does it have a subscription ID? | No; it is configuration-managed | Yes |
| Where do you inspect it? | Dev Dashboard app-version configuration | webhookSubscriptions |
| What does an empty API result prove? | It does not prove TOML configuration is missing | Check 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:Acmeis 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_fieldsis set, every field referenced by the filter must be included. Keepvendor. 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_atso 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
- 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.
- Apply changes in development.
shopify app devapplies 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. - Create an unreleased candidate. In the authorized project,
shopify app deploy --no-releasecreates an app version without releasing it to merchants. This command does not deploy a self-hosted web backend. Official deploy command and flag. - 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.
- 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'sX-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 action | Expected observation | First investigation if it differs |
|---|---|---|
| Change an Acme product's title | The event qualifies; look for its delivery | Effective version, grant, URI, and delivery logs |
| Change its title again without changing vendor | It still qualifies | Confusing a current-value condition with a change trigger |
| Change a non-Acme product's title | No corresponding filtered delivery in the observation window | Current vendor, effective filter, and parallel old subscriptions |
| Quickly change a field outside the retained field list | Compare payloads and updated_at | Identical 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.