Late payments aren’t a cash-flow accident, they’re a process failure. The average Indonesian SME invoices in rupiah while paying for tools in dollars — and a single Rp 15 million invoice stuck at 60 days eats more margin than most marketing efforts. But chasing people is awkward, so most freelancers and small shops just… don’t. They wait. Then they resent.
The fix isn’t more courage. It’s a 3-stage reminder flow that makes chasing impersonal, consistent, and automatic.
The 3-stage flow
- Day 0: Reminder goes out the day after the due date. Tone: friendly, assume they forgot. Most people pay here.
- Day 7: A nudge. Slightly firmer, still assumes good faith.
- Day 14: The firm one — recap, payment link, and a short “if you’d like to discuss payment terms, reply here” line that gives a face-saving exit.
Two numbers worth knowing: reminders sent on day 0 recover most invoices without any further contact, and firms that automate follow-ups report collecting roughly 20–40% of late invoices without escalating to a phone call or collections. The polite script works because people don’t ignore a specific, low-pressure email the way they ignore silence.
The script
Keep it dead simple: a CSV of invoices, a Python script, and a scheduler. No database, no framework.
import csv, smtplib, sys
from datetime import date
from email.message import EmailMessage
SMTP_HOST, SMTP_PORT = "smtp.mailgun.org", 587
SMTP_USER, SMTP_PASS = "postmaster@yourdomain.com", "your-api-key"
def stage(invoice_no, due_date, today):
days = (today - due_date).days
return 0 if days == 1 else 7 if days == 8 else 14 if days == 15 else None
def template(stage, name, invoice_no, amount, link):
if stage == 0:
subject = f"Reminder: Invoice {invoice_no}"
body = f"""Hi {name},
I noticed Invoice {invoice_no} ({amount}) was due yesterday — figured it's easy to slip between tasks. Payment link below, takes two minutes.
{link}
Thanks,
"""
elif stage == 7:
subject = f"Second reminder: Invoice {invoice_no}"
body = f"""Hi {name},
Following up on Invoice {invoice_no} ({amount}), still no payment on our side. If anything's off with the invoice, just reply — happy to sort it.
{link}
Best,
"""
else:
subject = f"Overdue: Invoice {invoice_no}"
body = f"""Hi {name},
Invoice {invoice_no} ({amount}) is now 14 days overdue. Please settle it this week. If you're having cash-flow trouble, reply and we can talk about payment terms — but we do need a response either way.
Pay here: {link}
Thanks,
"""
return subject, body
today = date.today()
sent = 0
for row in csv.DictReader(open("invoices.csv")):
d = row["due_date"].split("-")
due = date(int(d[0]), int(d[1]), int(d[2]))
st = stage(row["number"], due, today)
if st is None:
continue
subject, body = template(st, row["name"], row["number"],
row["amount"], row["pay_link"])
msg = EmailMessage()
msg["Subject"], msg["From"], msg["To"] = subject, SMTP_USER, row["email"]
msg.set_content(body)
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
s.starttls()
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg)
sent += 1
print(f"Sent {sent} reminders")
Run it daily and the day-0/week-1/week-2 stages fire automatically from the same invoices.csv. Columns: email, name, number, amount, due_date, pay_link. Wire a payment link (Xendit, Midtrans, or Stripe) into the CSV and the firmest email still stays low-pressure — you’re asking them to click, not to call.
Scheduling
- Linux (or a VPS):
cron—0 9 * * * cd /path/to/script && python3 remind.py >> remind.log - Windows: Task Scheduler → Create Basic Task → Daily → “Start a program” →
pythonw.exewith the script as argument. Run as your own user, at 9 AM, and tick “Run task only when user is logged on” if you want to see the logs. - Don’t have a machine on at 9 AM? A $5/mo VPS (about Rp 80,000) or a free tier runs it fine — worth it if your laptop’s Wi-Fi is your only always-on box.
Safety notes
- Test against a staging CSV first. Put your own email in there, verify the day math (send on day 1, 8, 15 after due date), then go live.
- Log everything. Append each send to a
history.csv. When a client disputes “I never got a reminder,” the log is your proof — and it keeps you honest about who you’ve hit. - Don’t spam. One email per stage, max three per invoice. Adding a day-3 “did you see this?” variant is how you train clients to ignore you.
- Watch the rupiah amounts. If you bill in IDR, format amounts with
Rpand thousands separators (Rp 15.000.000) — an unformatted15000000in the template is a small trust leak. - Acknowledge the first reply. If a client responds, the automated thread should stop — mark the invoice paid or deferred in the CSV so the script skips it. Automation handles the silence; humans handle the conversation.
Thirty minutes to build, and it keeps collecting from then on. The awkward part is no longer you — it’s a script, and scripts don’t feel awkward.