TL;DR
Sillage bills 1 800 regional carriers by card on the 1st of the month. Around forty charges fail — expired card, spending ceiling, a 3-D Secure prompt nobody completed. On day 14 the subscription is suspended and the fleets stop being tracked. Those fourteen days are the whole campaign. It has to be insistent about this invoice and forgetful about the last one.The situation
Sillage tracks delivery fleets for regional carriers. The product is invisible until it stops: when the subscription lapses nobody gets an alert — a dispatcher notices a van has gone off the map. Forty failed charges a month is too many to chase by hand and too few to justify a collections team. And a customer suspended over an expired card is a customer lost for a reason that had nothing to do with the product.Why “once per customer” is the wrong rule here
The migration deadline case solves the opposite problem: talk to each person once in their life, then never again. It is tempting to reach for the same recipe here. It fails twice. Pointing a campaign straight at the failure event. A declined charge is not one event. The provider retries on its own schedule and every failed attempt emits anotherinvoice.payment_failed — the payload carries attemptCount so you can watch it climb. Four attempts over ten days means four overlapping sequences for one invoice, each with its own day-3 reminder.
Adding a once-per-customer rule to fix it. Now March’s failure consumes the carrier’s only entry, April’s failure produces nothing at all, and the account is suspended in silence. You have swapped a noisy bug for a quiet one, which is worse.
The unit of this conversation is neither the attempt nor the person. It is the invoice.
Same primitive, one field apart. The grouping key is the scope of the campaign.
1. One entry per invoice
Create a Milestone synthetic event from Events → New event:- Event to count —
invoice.payment_failed - Milestones —
1 - Count per — all time
- Group by —
stripeInvoiceId
stripeInvoiceId reaches 1 invoice.payment_failed (all time), fire. One counter per invoice, one firing per counter — the second, third and fourth attempts increment a counter that has already fired and produce nothing.
Next month’s invoice carries a different id, so it opens a fresh counter and fires on its own first failure. Nothing resets and nothing has to remember March. (Counters do age out after about a year of inactivity — irrelevant for an invoice, which is settled or written off long before.)
The milestone carries the grouping key and the counter, not the original payload — your orchestrator reads
event.properties.stripeInvoiceId, thresholdReached and currentValue. That is deliberate: the amount owed and the payment link are fetched live at send time rather than frozen at the moment the card was declined.Closed beta. The Stripe source is one of the inbound connectors still marked beta — enabled per organisation, off by default. Ask your Notifizz contact to switch it on, or skip it: a campaign keys on the event name, so an
invoice.payment_failed your own backend emits with client.track() drives this page unchanged. See integrations.2. Four messages, one clock per invoice
The campaign subscribes to the milestone event and holds four messages:
All four go by email, and only by email. The person who has to act is the billing contact, who may never open the product — a message in the Notification Center would wait in an inbox nobody visits, and a web push would need a browser subscription they never granted. Email is the only channel that reaches someone by virtue of being a contact rather than a user.
Marketing owns the spacing and the copy; moving day 12 to day 10 is a save, not a release. Each invoice walks its own clock from the day its charge was declined.
This is a
Transactional campaign. Of the three categories it is the only one that carries no unsubscribe link, and that is the point: “your service will be suspended on Thursday” is not a mailing anyone should be able to opt out of. Product and Marketing are both promotional and honour the notification preferences; a dunning notice belongs in neither.
3. The recipient is recalculated at every send
The person who has to act is not the person using the product. The dispatcher who opens Sillage every morning is not the one holding the company card — the billing contact sits in an accounts department, and over twelve days that contact goes on holiday, changes job, or is replaced in the customer’s own settings. On the Recipients card, For follow-up sends, recipients are has two values. Keep the default:
The sequence is about the invoice, not about a person — so the audience is a question you ask again at every send, not an answer you cache on day 0.
For the developer: the enricher and the orchestrator
For the developer: the enricher and the orchestrator
One enricher, registered once in your backend with the Node SDK. It answers the only two questions the campaign has: who receives billing mail for this account right now, and is the invoice still owed.The orchestrator — AI-generated from the campaign description, then yours to review — pairs it with the connector’s built-in
stripe:invoice enricher, which returns the amount and the hosted payment link:account.settled comes from your system, not from the invoice enricher: stripe:invoice returns amountDue, currency, hostedInvoiceUrl, invoicePdf and the line items — an amount, not a verdict. Whether the debt is still open is your business rule, and it lives where your suspension date lives.The empty list is a real guard. A payment settled out of band — a wire transfer, a card retried by hand in the provider’s dashboard, an account closed by support — produces no recipient at all: the run lands in the Outbox as No recipients, and no message is created.4. Stop on the payment, and only on this invoice
On the Recipients card, set the objective toinvoice.paid and and ends when → Objective is reached. The remaining reminders of that invoice’s sequence are cancelled the moment the charge clears.
The payment event needs one extra step. Notifizz credits a conversion by matching a canonical identity — id, email, pushId or phone — against the recipients who received the campaign, and a payment provider identifies people by its own customer reference instead. So the orchestrator exports a second hook to resolve it:
stripeInvoiceId opens a different counter, the milestone fires, and the carrier gets the day-0 message as if it were the first time. No cooldown to age out, no flag to clear.
What Sillage sets up
- An
invoice.payment_failedevent carryingstripeInvoiceId— from the payment connector, or from its own backend - An
invoice.paidevent carrying the samestripeInvoiceId, wired as the campaign’s objective - One Milestone synthetic event, grouped on that invoice id
- One enricher,
billingContact, returning the live contact and the settled flag
Rehearsing it before it is Live
Open the milestone event: Milestone status lists the counters of your non-production environments, each with its current value against the next milestone — production counters are never shown here. Set puts a counter at the value you choose so the next event trips the milestone, Reset zeroes it; both are test actions, available on non-production environments only. Promote the campaign toReview on a test environment: that is the one status below Live where a real event still produces a delivery, and it reaches your sandbox testers rather than real people — see send scope. So you can fail a test invoice, watch the day-0 message land in your own inbox, mark it paid, and watch the day-3 message not arrive.
Reading the results
The Sends bar shows fewer messages delivered than the sequence intended, broken out by reason. Recipients dropped under Objective already reached are the carriers who paid: a success layer, not a warning, and a healthy campaign shows it growing message after message — day 3 reaches fewer people than day 0, day 7 fewer than day 3. The Outbox’s Objectives tab lists every evaluation ofinvoice.paid, whether or not it counted:
Already counted is the normal steady state of a recurring campaign, not a fault. Conversions are deduped per recipient per campaign: a billing contact credited for March’s late payment adds nothing to the count when they settle June’s. The stop obeys a different rule — it is claimed against the run in flight, every time. June’s reminders are cancelled exactly like March’s, even though the conversion figure does not move.
Troubleshooting
The invoice was paid but the next reminder went out anyway
The invoice was paid but the next reminder went out anyway
Open the Objectives tab and read the outcome. No identity means
onObjective is missing or returned null — without it there is no canonical id or email to credit. No message delivered is subtler: the identity resolved to the current billing contact, who changed since the last send and so never received a message from this campaign. Nothing to credit, nothing stops. That case is caught by the other guard — the orchestrator returns an empty list once your system reports the invoice settled.One carrier received four day-0 messages in ten days
One carrier received four day-0 messages in ten days
The campaign is bound to
invoice.payment_failed directly instead of to the milestone event. Every retry the provider makes is a fresh occurrence — check attemptCount on the payloads in the events catalogue, it will read 1, 2, 3, 4.Nothing fires at all
Nothing fires at all
Almost always the grouping key. If
stripeInvoiceId is absent from the payload, every failure collapses into one shared counter: threshold 1 fires once for the whole environment and no invoice ever triggers the campaign again. The Milestone status panel shows it plainly — one line per invoice is what you want to see, not one line total. Second suspect: the event name. A connector event carries the provider’s own type, verbatim — invoice.payment_failed, with no prefix added. Copy it from your event catalogue rather than typing it: the milestone and the campaign must key on exactly that string.Two sequences are running for the same carrier
Two sequences are running for the same carrier
Expected, and it follows from the scope: two unpaid invoices are two debts, so they get two conversations. To hold one conversation per account instead, group the milestone on the customer reference — but understand the trade. That counter fires once per customer for all time, so a carrier who fails in March and again in June is reminded once, in March.
The escalation kept going to someone who left the company
The escalation kept going to someone who left the company
The Recipients card is set to From the first send, so the contact resolved on day 0 owns the whole sequence and later arrivals are excluded — they appear in the per-send statistics under Not in the first send. Switch to Recalculated each send.
See also
Objectives & conversions
What counts as done, how it is credited, and why the stop is scoped to the run rather than to the person.
Synthetic events
The four types Notifizz generates for you, and what each one puts in
event.properties.Events & workflows
Recalculated each send versus from the first send, and how to pick by intent.
Migration deadline
The other scope: one entry per person for life, five reminders, stopped on the act.