Automated Invoice Payment Reminders


Chasing unpaid invoices manually is time-consuming. This script checks for overdue invoices daily and sends reminders via email or Slack.


Daily Reminder Script


python
#!/usr/bin/env python3
# invoice-reminders.py

import subprocess, json, smtplib, datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# Config
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
SMTP_USER = "billing@yourcompany.com"
SMTP_PASS = "your-app-password"
FROM_NAME = "Your Company Billing"

def ao(cmd):
    r = subprocess.run(["ao"] + cmd.split() + ["--json"], capture_output=True, text=True)
    return json.loads(r.stdout).get("data", [])

def send_email(to: str, subject: str, body: str):
    msg = MIMEMultipart("alternative")
    msg["Subject"] = subject
    msg["From"]    = f"{FROM_NAME} <{SMTP_USER}>"
    msg["To"]      = to
    msg.attach(MIMEText(body, "html"))
    with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
        s.starttls()
        s.login(SMTP_USER, SMTP_PASS)
        s.sendmail(SMTP_USER, to, msg.as_string())

overdue = ao("invoice list --status overdue --limit 100")
today   = datetime.date.today()

print(f"Found {len(overdue)} overdue invoices")

for inv in overdue:
    inv_num  = inv.get("invoice_number", inv["id"][:8])
    amount   = inv.get("total_amount") or inv.get("amount") or 0
    due_date = inv.get("due_date", "")
    contact  = inv.get("contact_email") or inv.get("customer_email", "")

    if not contact:
        print(f"  SKIP {inv_num}: no contact email")
        continue

    days_overdue = (today - datetime.date.fromisoformat(due_date)).days if due_date else 0

    subject = f"Payment Reminder: Invoice #{inv_num} is {days_overdue} days overdue"
    body = f"""
    <p>Dear Valued Client,</p>
    <p>This is a friendly reminder that invoice <strong>#{inv_num}</strong>
    for <strong>${amount:,.2f}</strong> was due on <strong>{due_date}</strong>
    and is now <strong>{days_overdue} days overdue</strong>.</p>
    <p>Please arrange payment at your earliest convenience.</p>
    <p>If you have already paid, please disregard this notice.</p>
    <p>Best regards,<br>{FROM_NAME}</p>
    """
    send_email(contact, subject, body)
    print(f"  Sent reminder for #{inv_num} to {contact}")

Slack Version


python
import urllib.request

SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK"

def slack_notify(text: str):
    data = json.dumps({"text": text}).encode()
    req  = urllib.request.Request(SLACK_WEBHOOK, data=data,
           headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req)

total = sum(i.get("total_amount") or 0 for i in overdue)
slack_notify(f":rotating_light: *{len(overdue)} overdue invoices* totalling *${total:,.0f}*\nRun `ao invoice list --status overdue` for details.")

Cron Schedule


bash
# Monday to Friday at 9 AM
0 9 * * 1-5 python3 /path/to/invoice-reminders.py >> /var/log/reminders.log 2>&1

Mark Batch Paid (after bank reconciliation)


bash
ao invoice list --status overdue --json \
  | jq -r ".data[].id" \
  | while read -r ID; do
      ao invoice mark-paid "$ID"
      echo "Marked paid: $ID"
    done

Tip: Check the days_overdue threshold before sending — you may want to only remind after 7+ days to avoid annoying clients with small delays.