I Automated the Late Payment Follow-Up (Here's the Script)

Published: (March 30, 2026 at 11:35 AM EDT)
3 min read
Source: Dev.to

Source: Dev.to

If you’ve been freelancing for more than a month, you’ve lived this: invoice sent, due date passed, radio silence.
The follow‑up email is easy to write once, but annoying to write on a schedule for every client, consistently. So most people don’t — and late payments drag on.

I automated it.

What the script does

It reads a CSV of your invoices (client name, amount, due date, email, status). Once a day, it checks for overdue invoices and sends the right email for the right stage:

  • Day 1 overdue – friendly nudge, warm tone
  • Day 7 – direct ask, invoice re‑attached
  • Day 14 – firmer, references previous emails
  • Day 30 – final notice before escalation

You set the templates once. The script handles the cadence.

Core logic (invoice_followup.py)

# invoice_followup.py — core logic
import csv, smtplib, datetime
from email.mime.text import MIMEText

STAGES = [
    (1,  "just checking in",     "warm"),
    (7,  "following up again",   "direct"),
    (14, "third notice",         "firm"),
    (30, "final notice",         "formal"),
]

def days_overdue(due_date_str):
    due = datetime.date.fromisoformat(due_date_str)
    return (datetime.date.today() - due).days

def should_send_today(days, last_sent_days):
    """Returns the stage template if today is a follow‑up day."""
    for threshold, subject_suffix, tone in STAGES:
        if days >= threshold and last_sent_days < threshold:
            return subject_suffix, tone
    return None, None

def run(invoices_csv, gmail_user, gmail_app_password):
    with open(invoices_csv) as f:
        rows = list(csv.DictReader(f))

    for row in rows:
        if row['status'] != 'unpaid':
            continue

        days = days_overdue(row['due_date'])
        last_sent = int(row.get('last_followup_days', 0) or 0)
        subject_suffix, tone = should_send_today(days, last_sent)

        if not subject_suffix:
            continue

        body = render_template(tone, row)
        send_email(
            gmail_user, gmail_app_password,
            to=row['client_email'],
            subject=f"Invoice {row['invoice_id']}{subject_suffix}",
            body=body
        )
        row['last_followup_days'] = days
        print(f"Sent {tone} follow‑up to {row['client_name']}")

    # Write updated CSV back
    with open(invoices_csv, 'w', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)

The render_template function fills in the client name, amount, and due date. The tone shifts from “friendly” to “formal” automatically as the invoice ages.

Setup (≈ 10 minutes)

  1. Export your invoices to CSV (any format — the script can be adapted).

  2. Create a Gmail App Password (2FA required, takes ~2 minutes).

  3. Run once:

    python3 invoice_followup.py invoices.csv
  4. Add to cron to run daily:

    0 9 * * * python3 /path/to/invoice_followup.py invoices.csv

That’s it. No SaaS, no subscription, no monthly fee—just a script that runs on your machine and sends emails from your own Gmail.

Customization options

  • Invoice tool – FreshBooks, Wave, Bonsai, plain CSV, Notion database
  • Email provider – Gmail, Outlook, custom SMTP
  • Escalation logic – different thresholds, tones, CC a third party at day 30
  • WhatsApp or SMS – send via Twilio instead of email
  • Slack notification – alert yourself when a follow‑up is sent or a payment arrives

Price: $25 flat. Delivered in 48 hours. One free revision.

If you want the generic version as‑is, it’s free — just ask in the comments and I’ll send the full script.

How to get it

  • Free version (generic): Comment below or email citriac@outlook.com — I’ll send the full script.
  • Custom version ($25): citriac.github.io/hire — describe your setup; I’ll scope it back within 24 hours.

Related: The Boring Work That Eats Your Freelance Hours — other repetitive things I automate for freelancers.

0 views
Back to Blog

Related posts

Read more »

Create issues from Slack with Copilot

!GitHub Copilot and Slack logos connected by bidirectional arrowshttps://github.blog/wp-content/uploads/2026/03/564403795-b093ced3-7d3b-4f86-9365-8c8d2dee0cd6.p...