Most subscription apps emit webhooks for the big lifecycle events — contract created, billing attempt succeeded, billing attempt failed, contract cancelled — but merchants rarely wire them into anything beyond the app's own analytics. That's a missed opportunity. Shopify Flow is a native automation engine that can consume subscription events and trigger downstream actions (tagging customers, sending emails, notifying fulfillment, updating Klaviyo segments) without any custom code. Combined with the webhook topics your subscription app and Shopify itself emit, you can automate most of the manual lifecycle work that otherwise eats support time. This guide covers which webhook topics matter, how to wire them into Flow, the automation patterns that produce real value, and the specific pitfalls around delivery reliability, retry behavior, and idempotency.
What Shopify Flow actually is, and where it fits
Shopify Flow is a visual automation builder bundled with Shopify Plus (and now available to non-Plus stores on most plans). You define a trigger, optional conditions, and actions — Flow does the rest. For subscriptions specifically, Flow can listen to subscription-related triggers (some emitted by Shopify directly, some by your subscription app) and execute actions: tag the customer, send an email, post to Slack, update metafields, call an external API, segment in Klaviyo or Mailchimp.
What Flow is good at: simple if-this-then-that lifecycle automations. What Flow is not good at: complex multi-step workflows with branching state, long-running jobs (waits longer than a few minutes), or workflows that require querying multiple APIs and stitching the results. For those, you want a webhook hitting your own backend or a Zapier/Make pipeline. Flow's sweet spot is the 90% of subscription automations that are 'when X happens, do Y' — and that's a lot of automations.
If the automation is single-step and stays inside Shopify (tag, email, segment), use Flow. If it crosses into your warehouse software, your CRM, or your data warehouse, use a raw webhook to your own endpoint. The line is roughly: does the action need to touch a non-Shopify system in a meaningful way? If yes, webhook. If no, Flow.
The webhook topics that matter for subscriptions
Shopify and subscription apps emit a stack of webhook topics relevant to the subscriber lifecycle. Not all of them are equally useful — some fire dozens of times per renewal, others only on rare events. Here's the working set most stores actually use.
- subscription_contracts/create — fired when a new subscription contract is created. The signal for 'welcome' automations.
- subscription_contracts/update — fired on any contract change (cadence switch, item swap, payment method update). Noisy; usually filter by what specifically changed.
- subscription_billing_attempts/success — fired when a renewal charge succeeds. The signal for 'order shipped soon' touchpoints.
- subscription_billing_attempts/failure — fired when a renewal charge fails. The single most important webhook for dunning automation.
- subscription_contracts/cancel — fired when a contract is cancelled (voluntary OR involuntary, usually distinguished by reason field).
- orders/create — Shopify-native, fires for every order including subscription renewals. Use the order tag or attributes to detect subscription orders.
- orders/fulfilled — Shopify-native, fires when fulfillment completes. Useful for post-fulfillment touchpoints.
- customers/update — Shopify-native, fires when customer profile changes (e.g. address update). Useful to propagate to your CRM.
The naming differs slightly between apps — some emit their own custom topics like 'subscription/paused' or 'subscription/resumed' that Shopify doesn't natively. Check your specific subscription app's webhook docs. The general principle holds: the lifecycle states (created, billed, billing-failed, cancelled, paused, resumed, updated) all need a webhook hook to be useful.
What a Flow trigger looks like under the hood
When Flow registers a trigger like 'Subscription contract created', it's subscribing to the underlying webhook on your behalf and parsing the payload into Flow's variable system. You don't see the raw JSON in Flow's UI, but it's helpful to understand the shape because it determines what fields you can reference in conditions and actions.
{
"subscription_contract": {
"id": 1234567890,
"customer_id": 987654321,
"status": "ACTIVE",
"next_billing_date": "2026-06-15T00:00:00Z",
"lines": [
{ "product_id": 111, "variant_id": 222, "quantity": 1 }
]
}
}In Flow, that payload becomes accessible as variables like {{subscriptionContract.id}}, {{subscriptionContract.customer}}, {{subscriptionContract.nextBillingDate}}, and {{subscriptionContract.lines}}. You use those variables in conditions ('if next_billing_date is within 3 days') and in action payloads ('tag this customer with active_subscriber'). The conversion from raw JSON to Flow variables is handled automatically — you only need to know which fields are available.
For raw webhooks (when you're not using Flow), the payload arrives as POST JSON to your endpoint with an HMAC signature in the headers. You verify the signature, parse the body, and act. Most subscription apps document the exact payload shape for each topic — refer to those docs for the canonical schema.
{
"billing_attempt": {
"id": 555,
"subscription_contract_id": 1234567890,
"status": "FAILED",
"error_code": "EXPIRED_CARD",
"error_message": "Card has expired",
"next_retry_at": "2026-05-25T12:00:00Z",
"attempts_count": 1
}
}Field names, nesting, and timestamp formats vary by subscription app. The shapes above are representative but not canonical. Before you build a Flow automation or a webhook consumer, find your app's docs page for the specific topic and verify the exact field names — every hour spent debugging 'why isn't this field populated' could have been saved by 5 minutes of doc-reading first.
The automation patterns that actually save time
Stores that get value from Flow + subscription webhooks tend to run the same five or six patterns. None of them are flashy; all of them either save support time or recover revenue. Build these first.
- Welcome on contract creation — trigger on subscription_contracts/create, tag the customer 'active_subscriber', send a welcome email with their first delivery date, segment them in your email tool. Saves your CRM from being out of sync.
- Dunning Slack alert on billing failure — trigger on subscription_billing_attempts/failure, post to a #dunning Slack channel with the customer name, error code, and retry date. Lets support team intervene proactively on high-value subscribers.
- Pre-renewal reminder for high-value contracts — trigger on subscription_contracts/update or a scheduled Flow checking next_billing_date, if the contract value exceeds your threshold, send the customer a 'your delivery is on the way' email 48 hours before charge.
- Cancel win-back trigger — trigger on subscription_contracts/cancel, add the customer to a Klaviyo win-back flow (30/60/90 day cadence). Recovers a meaningful single-digit percentage of cancellers.
- Fulfillment routing by SKU — trigger on subscription_billing_attempts/success, check the line items, if a specific SKU is included, post to your warehouse software's API. Handles edge cases that Shopify's native fulfillment routing can't.
- Cohort tagging for analytics — trigger on subscription_contracts/create, tag the customer with their signup-month cohort ('cohort_2026_05'). Makes cohort retention analysis possible later in tools that lack native cohorting.
Don't try to build all six on day one. Pick the one with the most obvious payback (usually 'dunning Slack alert' or 'welcome email') and build it end-to-end. Verify it fires, verify the action runs, verify nothing breaks. Then add the next one. Stores that try to launch ten Flow automations at once usually end up with five that silently broke and nobody noticed.
When Flow isn't enough: hybrid patterns
Flow handles 80-90% of subscription automation needs. The remaining 10-20% are workflows that need branching state, multi-step orchestration, or integration with systems Flow doesn't natively support. For those, the pattern is: use Flow as the trigger, have it call your own webhook endpoint, and do the complex logic there.
Concrete example: 'when a billing attempt fails AND the customer's lifetime value exceeds $500, send a personalized email from the founder.' The first part is a Flow condition. The 'lifetime value exceeds $500' check is also possible in Flow (via metafield lookup). But composing the personalized email content — pulling the customer's order history, their preferred products, the right founder signature based on the brand — that's beyond Flow's capability. Solution: Flow triggers on the failure, checks the LTV gate, then calls your endpoint with the customer ID. Your endpoint queries Shopify, composes the email, sends it via your transactional email provider.
- Flow as router — Flow handles the trigger and the simple condition, calls your endpoint for the complex part
- Flow + Zapier/Make — for non-engineering teams, Zapier or Make can sit between Flow's webhook action and external systems, with their own UI
- Direct webhook to your backend — bypass Flow entirely if the automation is purely backend (no Shopify-side actions like tagging)
- Scheduled Flow with custom query — for 'do this every day for subscriptions matching X' patterns, a scheduled Flow with a query step works
Delivery reliability, retries, and idempotency
Webhooks fail. Networks drop, your endpoint hiccups, Shopify's delivery system has occasional latency. Most webhook providers (Shopify included) retry failed deliveries with exponential backoff for up to 48 hours, then give up. That means your webhook consumer needs to be idempotent — the same event might be delivered twice, and your code should produce the same result both times without duplicating side effects.
Concrete example: on subscription_contracts/create, your handler sends a welcome email. If you don't dedupe by event ID or contract ID, a double-delivered webhook means two welcome emails to the same customer — confusing and slightly embarrassing. The fix is to store the event ID (or contract ID + event type) in a database table when you process the event; on receipt, check if you've seen this event already, and skip if so.
Shopify doesn't guarantee in-order delivery. You might receive a 'billing_attempt/success' webhook BEFORE the 'subscription_contracts/update' that announces the cadence change that just succeeded. Don't write logic that assumes webhook A always arrives before webhook B — your code needs to handle either order, ideally by querying the current state from Shopify when it matters.
- Verify HMAC signature on every webhook — the header is your only proof the request actually came from Shopify
- Dedupe by event ID — store seen event IDs for at least 7 days; skip duplicates
- Respond fast (under 5 seconds) — Shopify treats slow responses as failures and retries. Acknowledge immediately, do the work async if needed.
- Log everything — webhook arrival, processing outcome, errors. Webhook bugs are nearly impossible to debug without logs.
- Test the failure case — what happens if your endpoint is down for an hour? It should still recover when Shopify retries.
Three end-to-end examples
Three concrete automation flows merchants run today. Each is buildable in an afternoon and pays for itself within the first month.
Example 1: VIP subscriber alert. When a subscription contract is created with a total annual value above $500, post to a Slack channel and tag the customer 'vip'. Flow trigger: subscription_contracts/create. Condition: line_items.price × intervals_per_year > 500. Actions: customer tag 'vip', Slack message with customer details. Total build time: 20 minutes. Payback: support team knows to prioritize VIPs proactively.
Example 2: Smart dunning escalation. When a billing attempt fails twice in a row for the same contract, escalate beyond the standard dunning email by adding the customer to a Klaviyo 'high-risk' segment and sending a personal email from support. Flow trigger: subscription_billing_attempts/failure. Condition: attempts_count >= 2. Actions: Klaviyo segment update via webhook, Slack alert. Build time: 1 hour. Payback: recovers an additional 5-10% of involuntary churn vs default dunning.
Example 3: Cancel-reason routing. When a subscription is cancelled with reason 'product quality', tag the customer 'quality_complaint', post to a #quality-alerts Slack channel with the order history, and add them to a 'recover' email flow that includes a quality-control message and a discount on a different product. Flow trigger: subscription_contracts/cancel. Condition: cancel_reason == 'quality'. Actions: customer tag, Slack message, Klaviyo segment. Build time: 45 minutes. Payback: clusters quality complaints so you can spot supplier issues fast, AND recovers some cancellers.
- Listed the 5-6 lifecycle events that matter for your store
- Identified the webhook topic that fires for each one
- Picked Flow vs raw webhook based on the action complexity
- Verified the webhook signature on every consumer
- Made every consumer idempotent (dedupe by event ID)
- Built one automation end-to-end and verified it fires
- Logged the trigger, condition, and action outcome for debugging
- Documented each automation so the next merchant or developer on the team can read it
Common pitfalls when wiring subscriptions to Flow
Most automation projects start great and quietly degrade over months as edge cases pile up. The mistakes that cause this are predictable.
- Building automations that no one owns — six months later nobody remembers what 'Flow #7' does and whether it's safe to disable. Document every Flow in a shared doc with purpose, owner, and rollback steps.
- Triggering on /update without filtering — subscription_contracts/update fires on every change, including ones you don't care about. Always filter by the specific field that changed.
- No dead-letter handling — when your endpoint goes down for an hour, Shopify retries for 48 hours then gives up. You need a way to detect and replay the lost events, or you'll silently lose data.
- Ignoring rate limits — high-volume stores can hit Shopify's API rate limit from inside a Flow action (e.g. tagging customers in a loop). Build in throttling or async batching.
- Sending duplicate customer-facing actions — without idempotency a webhook double-delivery sends two emails. The customer notices.
- Hardcoding values that should be configurable — '$500 VIP threshold' should be a metafield, not buried in Flow condition logic. When you want to change it later you'll thank yourself.
- No monitoring on whether automations actually fire — Flow runs silently in the background. Add a periodic 'has Automation X fired in the last 7 days' check or you'll discover months later that it broke.
Subscription webhooks and Flow FAQ
Do I need Shopify Plus to use Flow?
Not anymore. Flow is now available on most Shopify plans, though some advanced triggers and connectors remain Plus-only. Check the Flow app listing in the Shopify App Store for your plan's specific feature set.
Which subscription apps emit webhooks usable by Flow?
Most native-API subscription apps (those built on Shopify's Subscription Contracts API) emit the standard subscription_contracts/* and subscription_billing_attempts/* topics that Flow can consume natively. Legacy or parallel-billing apps may emit only their own custom topics, which require Flow's webhook trigger configured manually.
Can I trigger a Flow from a custom subscription app event?
Yes, via Flow's HTTP request trigger. Your subscription app posts to a Flow-provided URL with a payload, and Flow's automation runs. Useful for events Shopify doesn't natively model (e.g. 'subscription paused', 'shipping date changed').
How do I test a Flow automation without affecting real customers?
Flow has a test mode for some triggers, but the most reliable approach is to use a test customer and test subscription on your dev store. Trigger the event manually (create a contract, simulate a failed payment) and watch the Flow run history.
What's the latency of a webhook-triggered Flow?
Typically a few seconds from event to Flow action. Worst-case can be 30-60 seconds during peak Shopify load. Don't build automations that require sub-second latency on webhook triggers — for that you need a different architecture (event streaming, polling).
Can I see the raw webhook payload that triggered a Flow?
In Flow's run history, you can inspect the variables that were passed to the automation, but not the literal webhook JSON. For full payload visibility, log it yourself in your subscription app or set up a webhook proxy (e.g. webhook.site) for debugging.
How do I avoid duplicate emails when a webhook is double-delivered?
Dedupe by event ID. Store seen event IDs (or contract_id + event_type combinations) for at least 7 days in a small lookup table. Before sending the email, check the table; if you've seen the event, skip.
What happens if my webhook endpoint is down?
Shopify retries with exponential backoff for up to 48 hours, then gives up. After 48 hours, the event is lost — Shopify won't redeliver it later. If your endpoint was down for longer, you'll need to manually backfill by querying Shopify for the missed events.
Can I chain multiple Flow automations?
Not directly. Each Flow is a single trigger-condition-actions unit. You can have Action A in Flow 1 produce a side effect (e.g. tag the customer) that triggers Flow 2 (e.g. 'customer tag added'), but the chain is implicit through side effects, not first-class. For complex orchestration, use your own backend.
How do I track whether automations are actually running?
Two options: (1) Flow's built-in run history shows recent executions per automation; check weekly. (2) Add a 'sentinel' step to each automation that logs a heartbeat to a tracking system (Slack, a simple log endpoint, an analytics event). If the heartbeat stops, the automation broke.
Can Flow trigger on subscription pause/resume events?
Depends on the subscription app. Pause/resume aren't native Shopify events — they're app-specific extensions on top of contract update. Your app needs to emit a webhook for the pause/resume action, and you wire it via Flow's HTTP trigger or via a subscription_contracts/update filtered to status changes.
Is there a security risk to exposing a Flow HTTP trigger URL?
The URL is a secret — anyone with it can trigger the Flow. Treat it like an API key: don't commit it to public repos, rotate if exposed. For higher-security needs, add HMAC signing on the calling side and verify in your Flow's first condition.