Skip to main content

Build a failed-payment recovery ladder

A failed payment is the canonical multi-channel automation: the customer owes you money, recovery odds fall with every day of silence, and each channel you add costs more than the one before. This tutorial builds a three-step recovery ladder in Devotel Orbit: transactional email the moment the charge fails, SMS on day 3, and a voice call on day 7, with the run ending as soon as the invoice is paid. You will emit the event from your billing system, enroll it into an event-driven journey, configure the channel ladder and its copy, decide what an opt-out on one leg does to the legs that follow, attach a conversion goal so recovered revenue is attributed to the step that closed it, and drill the failure paths in the sandbox. The ladder reuses the same primitives as Build an abandoned-cart recovery flow: flat flow variables, delay and condition nodes, and channel steps. The differences are the trigger (a billing event instead of a cart event), an escalating ladder instead of a single fallback, and the opt-out semantics. A payment reminder is account-critical, so a marketing opt-out must not silence it, and the tutorial shows you how to configure that posture deliberately.

1. Emit the event from your billing system

Your billing system (Stripe, Chargebee, Braintree, or your own invoicing service) is the source of truth for payment.failed and payment.succeeded. Post each failure to a webhook-triggered flow as soon as the charge fails:
Treat a 202 as an accepted trigger, not as proof that a message was sent. Sign the webhook according to Webhook security. Two rules keep enrollment clean:
  • Make retries replay, not re-enroll. When your queue worker retries the POST after a timeout, send an Idempotency-Key derived from the invoice and the failure count, as above. A replayed key returns the original response instead of starting a second ladder for the same invoice. The header contract, TTL windows, and failure modes are in Idempotent requests.
  • Emit the success event with the same key. When the retry succeeds, post payment.succeeded with the same invoice_id. Section 2 uses that event to end waiting runs.
Map your provider’s raw field names (invoice.id, charge.amount_remaining, customer.email) to the flat keys above at the edge. Event field mappings for integrations covers the mapping table for common billing providers. Keep the variables flat: {{first_name}} resolves in flow messages; dotted placeholders such as {{contact.first_name}} render blank.

2. Enroll on the event, exit on payment

A scheduled drip is the wrong intake for dunning. A drip sends on a calendar whether or not the invoice is open, so it either re-duns a customer who paid this morning or skips one who failed on an off-day. Event-driven enrollment ties one journey run to one invoice: the run starts when payment.failed arrives, carries the invoice key for its whole life, and ends the moment payment.succeeded arrives for that key. The enrollment and exit semantics are the same ones Build a campaign journey defines for event triggers, and Segment-triggered journeys shows the scheduled-intake contrast. Create the draft flow:
The no edge of each condition is deliberately unwired: when payment_open is no longer "true", the run ends without sending. Keep payment_open current by mapping the payment.succeeded event to the same invoice_id, the same way the abandoned-cart guide keeps cart_purchased current. If your billing system cannot update a waiting run, use the success event as a second trigger that cancels the matching run, scoped to invoice_id only. Never cancel every run for a contact: a customer can have a new open invoice while an older one is being recovered. Publish only after both branches are tested:
Send one test event with payment_open unset and confirm the run parks as waiting after the email leg; send a matching payment.succeeded and confirm the day-3 condition takes the unwired no path. Runs are accepted asynchronously (202) and audited per step with GET /api/v1/flows/executions/:id.

3. The ladder by attempt: windows and cost

Each attempt has its own channel, its own delay, and its own sending window: Preview the ladder’s cost before you publish. The journey simulator is read-only: it never enrolls a contact and never sends. Give it your own per-send rates and a realistic entry volume:
conditionPassRate is the share of runs still unpaid at each check; with 500 failed payments and 0.7, the projection shows roughly 350 SMS sends on day 3 and 245 calls on day 7, each with its spend. If the projected voice volume surprises you, widen the day-3-to-day-7 gap or raise the SMS copy’s payment-link visibility before you spend voice minutes. The ladder above escalates on time, not on delivery failure. If you also want a channel fallback inside one attempt (for example, WhatsApp when SMS reaches a terminal delivery failure), configure cascade_policy on the send step as described in Smart-send fallback chains. A fallback is a substitute for the same attempt, not a fourth attempt: it inherits the attempt’s consent and suppression checks.

4. Copy discipline per channel

Dunning copy fails when it reads like marketing. A customer who just had a card decline should get facts and a way to fix it, not a win-back offer.
  • Email (day 0): account-service tone. Name the invoice, the amount, and the fix. No cross-sell, no banner, no “we miss you” closer. Send it through your transactional stream so a marketing blast can never consume its throttle or drag its sender reputation: provision and pin the stream per Isolate transactional and marketing email streams.
  • SMS (day 3): one fact and one link. Identify your business, state the invoice and amount, and include the payment URL. Keep it to a single segment where possible; the amount label and short link are the two fields that push length.
  • Voice (day 7): a 20-to-30-second script that identifies your business and the purpose of the call, offers the payment URL by SMS or email, and stops there. Voice is the most intrusive leg; use it for the final attempt, not the first.
Per-step windows:
  • Email and SMS honor your tenant’s quiet hours. If a delay ends inside quiet hours, let the tenant send control defer the message to the next permitted window rather than adding an ad hoc scheduler; see Quiet hours configuration and Per-campaign quiet hours.
  • Voice is additionally bounded by the federal TCPA window, 8 AM to 9 PM in the recipient’s local time. The platform enforces this window on campaign and dialer voice regardless of tenant settings, and a call whose recipient timezone cannot be resolved does not go out. TCPA quiet hours and windows has the full reference.

5. Opt-out nuance: marketing vs account-critical

Dunning lives in the account-service message class, not the marketing class. The two classes are suppressed differently, and the difference is the whole point of this section. A marketing unsubscribe is not an account opt-out. A contact who left your promotional newsletter still owes you for the current invoice, and the transactional email leg above must still reach them. Keep the classes separate on every channel: separate email streams on the email side, and a preference center that lets a recipient drop marketing while account notices continue. What a STOP on the SMS leg does to later steps. Keyword handling is per channel. When the contact texts STOP, the consent flip applies to SMS: the send-time gate blocks any later SMS send to that contact, including missed_call_sms. It does not, by itself, stop the day-7 voice call, and it does not touch the email leg. Choose your suppression posture deliberately. Both postures below are tenant-owned controls; Orbit enforces whichever you configure.
  • Continue account-critical legs (the default shown here): honor the per-channel consent flip, keep the voice leg and the transactional email leg, and let opt-out rules handle the keyword side. This is the right posture when your terms require notice of past-due balances.
  • Halt the whole ladder on any opt-out: if your policy treats any STOP as “do not contact me further”, write a suppression record for the remaining channels through the Opt-Outs API, or attach a custom opt-out list that covers the senders this ladder uses. Every later step then fails its send-time check and the run winds down without sends.
Either way, message suppression still guards against the mechanical failure mode: two identical bodies to the same contact on the same channel inside the suppression window collapse to one send.

6. Conversion goal and attribution

Mark “paid” as the goal so the ladder reports revenue, not just sends. When you run the ladder as a campaign journey, upsert the goal once per campaign:
Upsert the goal before you wire conversion recording; a conversion posted to a campaign without a goal returns 404. Record each paid invoice from your billing listener, with the recovered amount in value_cents:
Then read the rollup:
The response returns total_enrolled, converted, conversion_rate, total_value_cents, and a per_step breakdown sorted by contribution. total_value_cents is your recovered revenue; per_step tells you which leg closed the most invoices, attributed to the last journey node the contact saw. Use the per-node counters at GET /api/v1/campaigns/:id/journey/node-analytics to find the step that bleeds contacts before they convert, and Reading journey paths when you want the path-level view.

7. Failure drill in the sandbox

Run these three drills before the ladder touches production traffic. Drill A: hard bounce on the email leg. Enroll a test contact with a mailbox that hard-bounces. Fetch the execution trace and confirm the email step ends in a terminal failure while the run still parks at the day-3 delay: the ladder escalates on time, so a bounce does not cancel later legs. Watch the bounce lifecycle webhook write the address to your email suppression list, per Email suppression event payloads; a second run for the same contact must not re-send to the bounced address. Drill B: STOP between day 3 and day 7. Enroll a contact, let the SMS leg send, then text STOP from the test handset. Confirm the consent flip in the contact’s consent history and that missed_call_sms would now fail its send-time check. Then apply whichever posture you chose in section 5: under the default posture the day-7 call still goes out inside the federal window; under the halt posture the suppression record stops it. Verify the outcome you configured, not the one you intended. Drill C: duplicate send inside the suppression window. Fire the same SMS body to the same contact twice within your suppression window and confirm the second send is skipped as a duplicate in the execution trace. This is the retry-safety net that lets your billing worker retry payment.failed deliveries without double-texting a customer. For all three, audit step by step:

Troubleshooting

Next steps