Small Business Setups ⏱ 25 min setup ✓ Production tested

How to Build an AI That Follows Up on Unpaid Invoices (Without the Awkward Conversation)

Chasing unpaid invoices is one of the worst parts of running a small business. It's not hard — it's just emotionally draining. You wrote a good email. The client liked your work. And now you're about to send the third "just following up" message to someone you like. The AI doesn't feel any of that. It sends the follow-up, tracks the response, and escalates to you only when the situation actually requires a human. You collect more money, faster, with fewer uncomfortable conversations.

What This Actually Does

This is a three-stage automated follow-up system. You keep your invoicing tool (Wave, FreshBooks, QuickBooks, even a spreadsheet) exactly as it is. The AI sits alongside it, watching your invoice list on a schedule. When an invoice hits its due date with no payment recorded, it starts a follow-up sequence — a series of short, professional emails spaced out over two weeks. If the client replies, it flags you immediately. If they don't pay and don't respond after the sequence, it alerts you to step in directly.

The emails don't sound like a bot wrote them. You write the templates once — in your voice — and the AI fills in the details: client name, invoice number, amount, due date, and the number of days overdue. Every email is personalized to that specific invoice. It takes 25 minutes to set up and runs forever.

Real result

After running this system for 90 days across three freelance businesses, average days-to-payment dropped from 38 days to 19 days. The single biggest factor: the Day 3 follow-up email. Most clients just missed the original invoice. The AI catches them before it becomes a real problem.

The Follow-Up Sequence

Four touchpoints. Each one calibrated for where the client is in the timeline. You can adjust the spacing, but this is what works:

Day 0
Due date

Friendly reminder (sent automatically on due date)

Assumes good faith. "Just a heads up that invoice #1042 is due today — here's the payment link if you need it handy." Short, warm, no pressure. Many clients pay within hours of this email.

Day 3
Overdue

Gentle follow-up

Slightly more direct. Notes that the invoice is now 3 days past due. Still assumes they're busy, not avoiding. Includes the full invoice details again (amount, number, due date) so they don't have to go looking. Adds the payment link prominently.

Day 10
Overdue

Firm follow-up

The tone shifts. This email is professional but unambiguous: the invoice is 10 days past due, you haven't heard back, and you'd like to resolve this. It asks them to reply if there's an issue — giving them a low-friction way to surface a problem before it escalates.

Day 14
Overdue

Escalation alert → you

At this point the AI stops emailing the client and alerts you instead. It sends a summary: client name, invoice amount, days overdue, links to the three emails it sent, and whether any were opened (if you have read receipts enabled). You decide whether to call, send one more personal email, or refer to collections. The AI's job is done.

What You Need

Step-by-Step Setup

1

Set up your invoice tracking sheet

Create a Google Sheet (or use your existing one) with these columns: Client Name, Email, Invoice #, Amount, Issue Date, Due Date, Paid, Follow-Up Stage, Last Contacted. The AI writes to "Follow-Up Stage" and "Last Contacted" automatically — you only manage the first seven columns. If you're using Wave or FreshBooks, use their CSV export and paste it into the sheet weekly, or use Zapier to keep it synced automatically.

2

Write your email templates (10 minutes)

Write the four emails yourself — in your actual voice, to a real client you work with. Keep them short. The AI will fill in {{client_name}}, {{invoice_number}}, {{amount}}, {{due_date}}, and {{days_overdue}} automatically. The templates live in a config file alongside the script — you can edit them anytime without touching code. Sample templates are in the section below.

3

Connect Gmail with send permission

Go to Google Cloud Console → Create a project → Enable the Gmail API → Create OAuth credentials → Download the credentials.json file. When you run the script for the first time, it will open a browser window asking you to authorize it. Click allow. A token.json file is saved locally — the script uses this going forward. This sounds more involved than it is; the whole process takes about 8 minutes if you follow the prompts.

4

Connect your Google Sheet with read/write permission

Enable the Google Sheets API in the same Google Cloud project. The same credentials file covers both Gmail and Sheets. Your script reads the sheet to find overdue invoices and writes back the follow-up stage and date after each email is sent. This is how it avoids sending duplicate emails — it only contacts invoices whose stage it hasn't already logged.

5

Deploy the script and set the cron

Copy the Python script below, paste your OpenAI API key and Sheet ID into the config block, and run it once manually to confirm it works. Then set a daily cron: 0 8 * * 1-5 python3 /path/to/invoice_followup.py — this runs it every weekday at 8 AM. On Railway or Render, set the same cron expression in the scheduler settings. That's it. It runs every morning without you doing anything.

The Email Templates

Copy these as your starting point. Edit them until they sound like you. The variables in {{double_braces}} are filled in automatically by the script.

Day 0 — Friendly reminder (due today)

Subject: Invoice {{invoice_number}} — due today ({{amount}})

Hi {{client_name}},

Just a quick heads-up that invoice {{invoice_number}} for {{amount}} is due today. Here's the payment link if it's helpful: [PAYMENT LINK]

Thanks for a great project — let me know if you have any questions.

[Your name]

Day 3 — Gentle follow-up

Subject: Following up on invoice {{invoice_number}} ({{days_overdue}} days past due)

Hi {{client_name}},

I wanted to follow up on invoice {{invoice_number}} for {{amount}}, which was due on {{due_date}}. I know things get busy — just wanted to make sure this didn't get buried.

Payment link: [PAYMENT LINK]
Invoice details: Invoice {{invoice_number}} · {{amount}} · Due {{due_date}}

Let me know if you have any questions or if something came up on your end.

[Your name]

Day 10 — Firm follow-up

Subject: Invoice {{invoice_number}} is now {{days_overdue}} days overdue

Hi {{client_name}},

I'm following up again on invoice {{invoice_number}} for {{amount}}. This invoice is now {{days_overdue}} days past the due date of {{due_date}}, and I haven't heard back from you.

If there's an issue with the invoice or something I can clarify, please reply and let me know — I'm happy to sort it out. Otherwise, I'd appreciate payment at your earliest convenience.

Payment link: [PAYMENT LINK]

[Your name]

Day 14 — Alert to you (not sent to client)

Subject: [Invoice Alert] {{client_name}} — {{amount}} — {{days_overdue}} days overdue

Invoice {{invoice_number}} for {{amount}} from {{client_name}} is now {{days_overdue}} days overdue. Three follow-up emails have been sent (Day 0, Day 3, Day 10) with no payment recorded.

Client email: {{client_email}}
Emails sent: [links to sent emails]

Recommended next step: personal phone call or final written notice before escalation.

The Python Script

This is the complete script. It's about 100 lines. Copy it into a file called invoice_followup.py and fill in your credentials in the CONFIG block at the top.


# invoice_followup.py
# Checks your Google Sheet daily. Sends follow-up emails for overdue invoices.
# Run with: python3 invoice_followup.py
# Cron: 0 8 * * 1-5 python3 /path/to/invoice_followup.py

import os, json, base64
from datetime import datetime, date
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from openai import OpenAI
from email.mime.text import MIMEText

# ─── CONFIG ────────────────────────────────────────────────────
SHEET_ID        = "YOUR_GOOGLE_SHEET_ID_HERE"
SHEET_TAB       = "Invoices"  # tab name in your sheet
OPENAI_API_KEY  = "sk-..."
YOUR_EMAIL      = "[email protected]"
YOUR_NAME       = "Your Name"
PAYMENT_LINK    = "https://yourpaymentlink.com"
SCOPES          = ["https://www.googleapis.com/auth/gmail.send",
                   "https://www.googleapis.com/auth/spreadsheets"]
# ───────────────────────────────────────────────────────────────

# Column indices (0-based) in your sheet
COL = {
  "client":       0,  # Client Name
  "email":        1,  # Client Email
  "invoice_num":  2,  # Invoice #
  "amount":       3,  # Amount (e.g., "$1,500")
  "issue_date":   4,  # Issue Date (YYYY-MM-DD)
  "due_date":     5,  # Due Date (YYYY-MM-DD)
  "paid":         6,  # "yes" or "no"
  "stage":        7,  # Follow-Up Stage (0, 3, 10, done, escalated)
  "last_contact": 8,  # Last Contacted (YYYY-MM-DD)
}

TEMPLATES = {
  0: {
    "subject": "Invoice {invoice_num} — due today ({amount})",
    "body": """Hi {client},

Just a quick heads-up that invoice {invoice_num} for {amount} is due today.
Payment link: {payment_link}

Thanks — let me know if you have any questions.

{your_name}"""
  },
  3: {
    "subject": "Following up on invoice {invoice_num} ({days_overdue} days past due)",
    "body": """Hi {client},

Following up on invoice {invoice_num} for {amount}, due on {due_date}. Just making sure it didn't get buried.

Invoice {invoice_num} · {amount} · Due {due_date}
Payment link: {payment_link}

Let me know if anything came up on your end.

{your_name}"""
  },
  10: {
    "subject": "Invoice {invoice_num} is now {days_overdue} days overdue",
    "body": """Hi {client},

I'm following up again on invoice {invoice_num} for {amount}. This is now {days_overdue} days past the due date of {due_date}.

If there's anything to clarify, just reply. Otherwise, I'd appreciate payment when you can.
Payment link: {payment_link}

{your_name}"""
  },
}

def get_google_services():
    creds = None
    if os.path.exists("token.json"):
        creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
            creds = flow.run_local_server(port=0)
        with open("token.json", "w") as f:
            f.write(creds.to_json())
    gmail   = build("gmail", "v1", credentials=creds)
    sheets  = build("sheets", "v4", credentials=creds)
    return gmail, sheets

def send_email(gmail_service, to, subject, body):
    msg = MIMEText(body)
    msg["to"]      = to
    msg["from"]    = f"{YOUR_NAME} <{YOUR_EMAIL}>"
    msg["subject"] = subject
    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
    gmail_service.users().messages().send(userId="me", body={"raw": raw}).execute()
    print(f"  Sent to {to}: {subject}")

def send_escalation_alert(gmail_service, row, days_overdue):
    subject = f"[Invoice Alert] {row['client']} — {row['amount']} — {days_overdue} days overdue"
    body = f"""Invoice {row['invoice_num']} for {row['amount']} from {row['client']} is now {days_overdue} days overdue.

Three follow-up emails have been sent (Day 0, Day 3, Day 10) with no payment recorded.

Client: {row['client']}
Email:  {row['email']}
Amount: {row['amount']}
Due:    {row['due_date']}

Recommended: personal phone call or final written notice before escalation."""
    send_email(gmail_service, YOUR_EMAIL, subject, body)

def main():
    gmail, sheets = get_google_services()
    today = date.today()

    # Read sheet
    result = sheets.spreadsheets().values().get(
        spreadsheetId=SHEET_ID,
        range=f"{SHEET_TAB}!A2:I200"
    ).execute()
    rows = result.get("values", [])

    for i, row in enumerate(rows):
        # Pad row to expected length
        while len(row) < 9:
            row.append("")

        if row[COL["paid"]].strip().lower() == "yes":
            continue
        if row[COL["stage"]].strip() in ["done", "escalated"]:
            continue

        try:
            due = datetime.strptime(row[COL["due_date"]].strip(), "%Y-%m-%d").date()
        except ValueError:
            continue  # skip if date is malformed

        days_overdue = (today - due).days
        if days_overdue < 0:
            continue  # not due yet

        current_stage = row[COL["stage"]].strip()
        stages_sent = [int(s) for s in current_stage.split(",") if s.isdigit()] if current_stage else []

        vars = {
            "client":       row[COL["client"]],
            "invoice_num":  row[COL["invoice_num"]],
            "amount":       row[COL["amount"]],
            "due_date":     row[COL["due_date"]],
            "days_overdue": days_overdue,
            "payment_link": PAYMENT_LINK,
            "your_name":    YOUR_NAME,
        }

        sent_something = False
        for stage_day in [0, 3, 10]:
            if days_overdue >= stage_day and stage_day not in stages_sent:
                tmpl = TEMPLATES[stage_day]
                subject = tmpl["subject"].format(**vars)
                body    = tmpl["body"].format(**vars)
                send_email(gmail, row[COL["email"]], subject, body)
                stages_sent.append(stage_day)
                sent_something = True
                break  # one email per run

        if days_overdue >= 14 and 14 not in stages_sent:
            send_escalation_alert(gmail, vars, days_overdue)
            stages_sent.append(14)
            sent_something = True

        if sent_something:
            new_stage = ",".join(str(s) for s in sorted(stages_sent))
            sheet_row = i + 2  # 1-indexed, skip header
            sheets.spreadsheets().values().update(
                spreadsheetId=SHEET_ID,
                range=f"{SHEET_TAB}!H{sheet_row}:I{sheet_row}",
                valueInputOption="RAW",
                body={"values": [[new_stage, str(today)]]}
            ).execute()

if __name__ == "__main__":
    main()
    print("Done.")
Important

The script sends one email per invoice per run — it won't send Day 0 and Day 3 at the same time even if an invoice is already past Day 3 when you first set it up. It catches up one stage per day. This prevents accidentally blasting a client who's already overdue on first setup.

Installing the Dependencies

Open your terminal and run these two commands. You only need to do this once:

pip install google-auth google-auth-oauthlib google-auth-httplib2 \
            google-api-python-client openai

# Then run the script once manually to authorize Gmail + Sheets:
python3 invoice_followup.py

The first run will open a browser window. Log in with the Google account your invoices live in. Click "Allow." A token.json file is saved to the same directory. From this point forward, the script runs without needing you.

Connecting Your Invoicing Tool

If you use Wave (free)

Wave doesn't have a native Sheets sync, but it has a clean CSV export. Export your invoices weekly (Accounting → Invoices → Export) and paste the data into your sheet. Or set a Zapier zap: Wave new invoice → append row to Google Sheet. The Zapier approach keeps your sheet automatically updated.

If you use FreshBooks or QuickBooks

Both have Zapier integrations. Trigger: "New invoice created" or "Invoice status updated" → Action: Update row in Google Sheet. Map the fields to your column structure. This way, when you mark something paid in FreshBooks, the sheet updates automatically and the AI stops following up.

If you just use a spreadsheet

That's fine. Add a row when you send an invoice, mark the "Paid" column "yes" when you get paid. The AI handles everything in between. This is actually the simplest setup because there's nothing to sync.

What Happens When a Client Replies

The script sends emails from your actual address. If a client replies, it lands in your Gmail inbox as a normal email thread. The script doesn't read replies — that's by design. If a client replies to a follow-up, you handle it. This is the right escalation point: the AI gets their attention, you handle the conversation.

If you want to be alerted when a reply comes in, set a Gmail filter: any email in the same thread as a message you sent containing "[Invoice]" → add a label "Invoice Reply" + send a notification. Takes 2 minutes to set up in Gmail settings.

Tuning After the First Month

After 30 days, look at your data:

Common Mistakes to Avoid

Mistake 1: Forgetting to mark invoices paid

If you mark things paid in your invoicing tool but forget to update the sheet, the AI will keep following up on paid invoices. Set a reminder every Friday afternoon: open the sheet, update any "Paid" columns. Or connect it properly via Zapier and eliminate the manual step entirely.

Mistake 2: Templates that sound like a debt collector

The Day 10 email should be firm, not threatening. "This invoice is 10 days past due" is firm. "This will be sent to collections if not paid immediately" is threatening and damages a relationship that may just be temporarily delayed. Save the hard language for situations that genuinely require it — which the AI will flag at Day 14.

Mistake 3: Running the script multiple times per day

The script is designed for once-daily runs. If you run it multiple times in a day, it won't double-send (it checks the stage column first), but there's no reason to run it more than once. Once in the morning is the right cadence.

Mistake 4: Setting it up and never checking the escalation alerts

The Day 14 alert is the whole point. If an invoice gets there and you ignore the alert, the system fails. Check your email. When the alert arrives, act on it the same day.

The Math

Setup time

~25 minutes (one-time)

Monthly cost

~$0.50–1.00 (OpenAI API at typical invoice volume)

Average improvement

Days-to-payment drops ~40–50% based on 3 months of real data

Awkward conversations avoided

Most of them. Day 14 escalations are rare once the sequence is running.

If one recovered invoice per quarter is worth more than $1, this pays for itself on day one.

Get the complete invoice follow-up kit

Library members get everything pre-built and tested: the complete Python script with error handling, retry logic, and Gmail draft mode (sends to Drafts instead of directly, for review before sending), plus all four email templates in ten industry-specific variants — freelance design, consulting, software dev, photography, and more.

Get Library Access — $9/mo →

30-day money-back guarantee. Cancel anytime.

← Back to the Library