Overdue Invoice Chaser
Module 21 · business · intermediate
Lesson 21 · Overdue Invoice Chaser
Goal
Late payments hurt small businesses, and chasing them is awkward, so it gets forgotten. Build a chaser that sends the right reminder at the right time, stops when it should, and tells the owner where the money is.
Prerequisites
- Modules 05 (Gmail), 13 (static data), 15 (injected clock, per-stage rules), 19 (scheduler instead of Wait nodes)
- For the live part: a Stripe account (test mode is fine) and Gmail
Concepts
- No Stripe node for invoices? Use HTTP Request with the Stripe credential. Authentication → Predefined Credential Type → Stripe API signs any Stripe REST call:
GET https://api.stripe.com/v1/invoices?status=open&collection_method=send_invoice. Amounts come back in cents, dates as Unix seconds. - Stages relative to the due date. Each stage has a day offset (
-3, 0, 3, 10, 21, 30). Each run computes days past due in the business's timezone and picks the latest stage that is due and not yet sent. - Catch-up without spamming. If the workflow was off for two weeks, a 10-day-late invoice gets only the second reminder, never four emails at once. The skipped stages are reported.
- A ledger, not a Wait node. Static data records which stage was sent when. That's how the chaser knows not to repeat itself, keeps a 3-day minimum gap, and escalates only 7 days after the final notice.
- Humans can pause it. A promise to pay pauses until a date; a dispute pauses indefinitely. Nothing is more damaging than an automated "FINAL NOTICE" to a customer who already called you.
- Tone ladder. Friendly heads-up → due today → "easy to miss" → "when can we expect payment?" → final notice (owner cc'd) → internal escalation to the owner (no customer email). Every email offers a way to reply about problems.
- Import safely. Turning this on for an existing backlog would email everyone at once.
record_only: truemarks today's reminders as sent without emailing, so the next run continues from there.
Build it
Step 1: Triggers, Settings, Route
- Schedule
Weekdays 9 AM(cron0 9 * * 1-5), WebhooksChase Run API(POSTar/run),Pause API(POSTar/pause),Status API(GETar/status). - Edit Fields
Settings (edit me): business and sender name, owner email, currency, timezone,stages,min_gap_days,min_amount,do_not_chase(emails or domains), payment instructions (used when there's no pay link), final-notice note, summary day. - Switch
Route: run / pause / status.
Step 2: Load invoices
- IF
Invoices from API?(API call withoutsource: "stripe"). - HTTP Request
Get Stripe Invoices: GEThttps://api.stripe.com/v1/invoices, Stripe credential, querystatus=open,collection_method=send_invoice,limit=100; On Error → Continue. - Code
Normalize Invoices(normalize.js): one list withinvoice_id, number, customer, email, amount_remaining, currency, due_date, status, pay_url, from the API body (simple or Stripe format) or the Stripe response; a failed fetch becomessource_error.
Step 3: Plan and send
- Code
Plan Reminders(plan.js): skip rules → days past due (Luxon, timezone) → latest due stage → final-before-escalation → gap check → compose the email. Returns one item per email (and the full plan on the first item). - IF
Send Now?(has an invoice, not dry run, not record-only) → GmailSend Reminder(To, Subject, Text, Cc from the plan), On Error → Continue. - Code
Record and Summarize(record.js): a stage is marked sent only when Gmail returned a message id (failures retry next run); aging buckets (current, 1-30, 31-60, 61-90, 90+), largest overdue, summary text. - IF
Called via API?→ Respond. IFSummary Day?→ GmailEmail A/R Summaryto the owner.
Step 4: Pause and status
Code Pause Invoice (pause.js) → Respond (400 without an id). Code Invoice Status (status.js) → Respond (404 if unknown).
Publish (static data only persists for production executions).
Test it
./test/test.sh # 60 assertions, no credentials
WITH_CREDS=1 TEST_EMAIL=you@example.com ./test/test.sh # real reminder email (STRIPE=1 also fetches Stripe)
Preview a run without sending anything:
curl -X POST http://localhost:5678/webhook/ar/run -H 'Content-Type: application/json' -d '{
"dry_run": true, "now": "2026-10-14T14:00:00Z",
"invoices": [{"id":"in_1","number":"NW-1001","customer":"Maya Patel","email":"maya@acme.example","amount_remaining":1200,"due_date":"2026-10-04"}]}'
Common problems
- Every customer emailed on day one: you turned it on over a backlog. Run once with
record_only: truefirst. - Reminder a day early or late: check
timezonein Settings; due dates are compared in that zone. - Stripe returns nothing: invoices charged automatically (
charge_automatically) have no due date and are excluded; onlysend_invoiceinvoices are chased. - More than 100 open invoices: Stripe pages its results (
has_more: truein the response). Turn on pagination in the HTTP Request node (Exercise 3). - Reminder sent after the customer replied: nothing pauses on replies yet (Exercise 1).