The Hard-Bounce Firewall Your Shopify App Probably Doesn’t Have
This playbook walks through a hard-bounce suppression journey for Shopify and e‑commerce apps, showing how to tag, suppress, and surface bounced contacts so your sender reputation stays clean and your nurture campaigns actually reach merchants.
Industry
Niche
Pattern
Loading sequence...
CartWizard thought they had email figured out.
Their Shopify cart recovery app sent thousands of onboarding and feature emails each week. Trials were flowing in, MRR was climbing, and then… metrics started to drift. Open rates slid. Clicks softened. A few merchants even asked why they missed important billing emails.
The culprit was boring and invisible: hard bounces that never got cleaned up. Dead inboxes stayed on the list, campaigns kept trying to reach them, and their sender reputation paid the price. Every future email to real customers got a little harder to deliver.
That’s what this article is about: building a tiny “firewall” around hard bounces so they can’t quietly poison your list.
The sequence at the top of this page is the whole journey, end to end. It listens for a confirmed hard bounce, tags that contact, suppresses them from marketing, and notifies someone on your team to check what happened.
For a Shopify app or e‑commerce tool where most revenue comes from long-term subscriptions, this is a classic lifetime-value leak. You can’t nurture or retain customers if your emails never make it to the inbox.
Let’s fix that.
Why hard bounces are more than a deliverability problem
For an e‑commerce app, hard bounces usually come from three buckets:
Merchants using a throwaway or typoed email during install.
Old champions who left a company and had their mailbox deleted.
Stores that churned quietly and whose inbox now returns “user unknown.”
Individually, those look harmless. But at scale:
ESPs factor bounce history into your reputation.
Inbox providers (Gmail, Outlook) become more conservative about accepting your traffic.
Future campaigns to your good merchants land in Promotions or spam more often.
That doesn’t just hurt list “quality.” It undermines your core growth loop: trial-to-paid conversion, expansion, and reactivation.
The reality for founder-led SaaS is simple: most of the lifetime value you could earn is lost because engagement decays over time. If your messages don’t arrive, you can’t run the nurture and win-back plays that keep merchants active.
A hard-bounce suppression flow is one of those small automations that quietly protects everything else. You build it once, then forget it while your email reputation stays cleaner.
What this hard-bounce suppression journey actually does
At a high level, the journey in the visual sequence does three things when a confirmed hard bounce happens:
List hygiene: It tags the contact so you can see they hit a bounce and report on it later.
Suppression: It immediately stops further marketing by updating their email subscription status and, optionally, their marketing status.
Operational visibility: It sends an internal alert so someone on your team can decide whether to fix an address, contact the account owner through another channel, or just leave them suppressed.
All of that runs automatically whenever your app or email provider reports a hard bounce via a custom event.
Let’s walk through each node and why it’s configured the way it is.
Trigger: Custom Event “email_bounced”
This journey is built as a Journey, not a one-time campaign, because it needs to run continuously for every new bounce. You’d create it in the journeys section using the same editor described on the campaigns and journeys platform page.
The entry point is a Custom Event trigger configured roughly as:
Event name: something like
email_bouncedRe-enrollment: off
Property conditions:
type is "hard"
Why this setup:
Custom Event trigger is the right fit because your bounce signal comes from outside Spreeflo: your SMTP provider, your app’s own mailer, or a third-party service. When they detect a confirmed hard bounce, they call
Spreeflo.track("email_bounced", { type: "hard" })or hit the Spreeflo API server-side.Filtering to
type = hardat the trigger level keeps this journey laser-focused. You don’t want soft bounces, temporary issues, or throttling glitches to suppress a contact forever.Re-enrollment is off because a single confirmed hard bounce is enough to act. After the first run, the contact is safely suppressed and further bounce events don’t need to re-fire the flow.
If you later want to handle soft bounces differently, you can create a second journey with its own Custom Event trigger filtered to type = "soft" and different suppression rules.
Step 1: Add Tag – make bounces visible everywhere
Right after the trigger, the journey uses an Add Tag node.
Configuration idea:
Tags:
email-bounced-hardForce tag trigger: off
Why tag first:
Tags are the simplest, most flexible way to make bounces visible across your workspace. Once you tag contacts this way, you can exclude them from ad-hoc campaigns, build segments around them, or even create win-back experiments later.
Keeping
Force tag triggeroff is usually right here. If the tag is already present, you don’t need to trigger any Added Tag flows again.
With that in place, you can later define a segment like “All contacts tagged with email-bounced-hard” using the segment builder. That’s helpful for reporting and sanity checks.
If you’re not yet using tags aggressively, it’s worth skimming the short guide on getting started with tags. This pattern leans on tags as the glue between journeys and reporting.
Step 2: Update Email Subscription Status – stop marketing sends
Next, the journey drops into an Update Email Subscription Status node.
Configuration:
Status: Email unsubscribed
This does the most important job in the whole flow: it tells Spreeflo’s sending engine “do not send any more marketing email to this address.”
Why unsubscribed, not non-subscribed:
A hard bounce is a clear, one-way signal that this address is not deliverable. From a deliverability and compliance perspective, it’s safer to treat this like an opt-out than a “we’re not sure yet” status.
It also reduces the risk that your team accidentally re-adds the contact to a campaign later. Unsubscribed is a clear, explicit state.
This node is what actually protects your sender reputation over time. Every additional campaign that doesn’t attempt to hit a dead mailbox is a win.
Step 3: Update Contact Attribute – marketing status and timestamps
You already cut off email sends, but there are two more smart moves:
Move the contact out of your marketing universe.
Record when the bounce happened.
The journey uses an Update Contact Attribute node to do both, usually with two separate nodes for clarity.
a) Marketing status
Attribute: the built-in “Marketing Status”
Update type: Update
Value: Non-marketing
This keeps your count of “marketing contacts” clean for billing and reporting, as described in the pricing-plan explainer. Someone whose email no longer works shouldn’t count against your marketing-contact limits.
b) Last hard bounce timestamp
Attribute: a custom TIMESTAMP attribute, e.g.
last_hard_bounce_atUpdate type: Set to now
Value: Set to now
This gives you a single, reliable timestamp whenever the node executes. Because “Set to now” is computed at runtime, you don’t have to hard-code a date.
A crucial detail: Update Contact Attribute writes static values you choose at design time, with the single exception of “Set to now” on timestamp attributes. If you want to store details like bounce_reason or smtp_code from the event payload, you’d have your backend call Spreeflo.identify with those attributes at the moment the bounce occurs instead of trying to copy them inside this journey.
Step 4: Optional tagging for ops workflows
Some teams like a lighter tag for marketers and a more operational tag for engineering or support.
If that’s you, drop in a second Add Tag node after the attribute updates:
Tags:
requires-email-revieworinvalid-email
Later, you can build an internal dashboard that filters to these tags and lets someone clean up addresses, look up account owners in your own admin, or coordinate with success.
The journey ends the same way regardless; this extra tag just makes owning the bounce backlog easier.
Step 5: Send Internal Email – alert the humans
The last active step is a Send Internal Email node, which pings whoever on your team should care about hard bounces.
Configuration:
Recipient: a shared inbox like
ops@yourapp.comor your support addressSend only once: on
What goes in the email:
The merchant’s name and email address (from contact attributes)
The fact that a hard bounce was detected
The timestamp and, if you store it as a contact attribute, the bounce reason
A short checklist: “If this is a high-MRR account, verify the right billing contact in your admin and update their email.”
Setting “Send only once” protects your team from alert fatigue. Even if a misconfigured integration accidentally fires the same bounce event twice, they won’t get duplicate messages for that contact.
This step is where the “ops response time” metric lives. Once the email lands, it’s on your team to decide whether the bounce is expected (e.g. churned store) or needs action (e.g. high-value customer with changed company email).
If you want to wire this into other systems, you can add a Webhook node either before or after the internal email to push the same data into a Slack automation or internal tool. That’s optional, but nice if your team already lives in a chat-driven workflow.
How the bounce event reaches this journey
Nothing in this flow works if the bounce event never arrives.
There are two common patterns e‑commerce app teams use:
From your mail provider. If you send emails via your own SMTP or transactional provider, configure its webhook to hit your backend with bounce data. When you see a confirmed hard bounce for a marketing email, have your app call the Spreeflo API with an
email_bouncedcustom event and the appropriatetype: "hard"property. Optionally, also callSpreeflo.identifyto store structured details likebounce_reasonon the contact.From your app directly. If you already track message failures in your own database (for example, app notifications that map to email), fire a custom event whenever an address moves into a “permanently failed” status.
Once that pipeline is in place, you don’t need to touch it again. Every new hard bounce drops into the journey, gets suppressed, tagged, and surfaced to your team automatically.
Measuring whether the flow is doing its job
You can’t improve what you don’t track, so tie this journey back to three concrete metrics.
Bounce rate
Look at your ESP or Spreeflo’s email analytics before and after enabling the flow. You should see:
- Total marketing bounce rate trend down slightly as cleaned addresses fall out of future sends.
- New campaigns with more stable, predictable deliverability.Suppression accuracy
Use a segment like “Tag containsemail-bounced-hardAND Email Subscription Status is Unsubscribed” in the segment builder. Over time, the count of hard-bounce tags should match the count of unsubscribed contacts for that reason. Any gap suggests a misconfigured node or an integration bug.Ops response time
This one is more manual but still valuable. In your help desk or internal task tracker, tag tickets created from bounce alerts. Measure the lag between when the journey fires (usinglast_hard_bounce_at) and when someone closes the associated task. For high-value accounts, you can set a simple SLA like “investigate within 2 business days.”
The win is subtle: emails quietly start landing more often for the merchants who matter. Trial nurtures reach their inbox, expansion offers get seen, and you stop burning deliverability on addresses that will never convert.
Variations and pitfalls to avoid
A few patterns show up repeatedly across SaaS and Shopify apps.
Smart variations:
Soft bounce retry logic. Build a separate journey for
email_bouncedevents wheretype = "soft". Use a Custom Event trigger, a Wait Condition that looks for “email delivered at least once in the last X days,” and only suppress after three consecutive soft bounces.Spam-complaint suppression. Use an Email Action trigger with
emailAction = repliedonly if you support “reply to unsubscribe,” or wire your complaint feedback loop to fire aspam_complaintcustom event and treat it as an immediate unsubscribe with a similar flow.
Pitfalls to avoid:
Suppressing on any bounce without checking type. That’s how you accidentally unsubscribe half your list during a short-lived DNS outage.
Forgetting to cut off future sends. Tagging alone doesn’t stop campaigns unless you also gate sends on Email Subscription Status or Marketing Status.
Over-notifying ops. If your app has a lot of low-value free users, you may want the internal email to fire only when
plan is not freeorMRR >= $X, which you can express with an If/Else node before Send Internal Email.
A small flow that protects your ability to nurture
Hard-bounce suppression doesn’t feel glamorous. It doesn’t move the install rate graph on its own, and nobody tweets screenshots of bounce dashboards.
But it quietly protects the thing that does move revenue: your ability to keep showing up in merchants’ inboxes with timely, relevant messages.
For founder-led, under-10-person teams, this is where leverage comes from. You put a few nodes in place once, the system runs forever, and your future campaigns work better as a result.
If you already have a journey canvas open, you can build this exact pattern from the sequence at the top of this page in under an hour. Or grab a similar starter from the template library and adapt it to your own events.
Either way, treating hard bounces as a first-class event in your automation is one of those quiet decisions that compounds every time you hit “send.”