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.
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.
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.
Four touchpoints. Each one calibrated for where the client is in the timeline. You can adjust the spacing, but this is what works:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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]
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]
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]
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.
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.")
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.
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.
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.
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.
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.
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.
After 30 days, look at your data:
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.
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.
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.
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.
~25 minutes (one-time)
~$0.50–1.00 (OpenAI API at typical invoice volume)
Days-to-payment drops ~40–50% based on 3 months of real data
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.
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.
30-day money-back guarantee. Cancel anytime.