Skip to main content

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 another invoice.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 countinvoice.payment_failed
  • Milestones1
  • Count perall time
  • Group bystripeInvoiceId
Read it as the dashboard states it back to you: when a 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.
That exemption has teeth. A recipient who cannot opt out and marks the mail as spam is suppressed permanently by the email provider, and that cannot be undone. Keep the sequence to the invoice: no discount, no “we miss you”, no upsell to the annual plan. A payment reminder carrying a promotion has stopped being transactional, whatever the category selector says.

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.
Recalculating is not the same as following someone through a change. Notifizz holds no customer data: a sequence tracks each recipient by the id your orchestrator returns, so a changed contact is simply a different recipient — which is the intent here. It also means your ids must be stable for everyone who did not change. Never fall back between fields (u.id ?? u.email), or an ordinary CRM sync looks exactly like a change of contact. See how recipients are recognised.
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 to invoice.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 identityid, 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:
Write it only when the objective event carries no canonical identity — otherwise the fast path runs and there is nothing to configure. See objectives. And then nothing else happens. Paying March’s invoice cancels March’s reminders and changes nothing beyond them: the stop applies to the sequence in flight, not to the person. In April a different 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_failed event carrying stripeInvoiceId — from the payment connector, or from its own backend
  • An invoice.paid event carrying the same stripeInvoiceId, 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
No customer data moves into Notifizz: the contact is read at send time and forgotten, which is also why a change of accountant needs no synchronisation on anyone’s part.

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 to Review 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 of invoice.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

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.
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.
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.
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 failure event does not carry one — it exposes the amount, the attempt count and the next attempt date, but no link; only the paid event carries a hosted invoice URL. Fetch it at send time with the stripe:invoice enricher, which returns hostedInvoiceUrl alongside the amount — the same call that keeps the quoted total honest twelve days later.
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.