Industry Applications
legal
How to Create Automated Legal Billing and Time Tracking
Track billable hours and generate invoices for law firms automatically.
Jay Banlasan
The AI Systems Guy
Automating legal billing and time tracking fixes the revenue leak that every firm has. Attorneys forget to log time, descriptions are vague, and invoices go out late. I built this system to capture time entries as work happens, generate clear billing descriptions, and produce invoices automatically at the end of each billing cycle.
What You Need Before Starting
- Python 3.8+
- Anthropic API key for description generation
- SQLite
- A list of billing rates by attorney and task type
Step 1: Set Up the Billing Database
import sqlite3
from datetime import datetime
def init_billing_db(db_path="legal_billing.db"):
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS time_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
attorney TEXT,
client_id INTEGER,
matter TEXT,
description TEXT,
hours REAL,
rate REAL,
amount REAL,
billable INTEGER DEFAULT 1,
status TEXT DEFAULT 'unbilled',
date TEXT,
created_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER,
client_name TEXT,
invoice_number TEXT,
total_hours REAL,
total_amount REAL,
period_start TEXT,
period_end TEXT,
status TEXT DEFAULT 'draft',
created_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS billing_rates (
attorney TEXT,
task_type TEXT,
rate REAL,
PRIMARY KEY (attorney, task_type)
)
""")
conn.commit()
conn.close()
Step 2: Build the Time Entry System
def log_time(attorney, client_id, matter, raw_description, hours, task_type="general", db_path="legal_billing.db"):
conn = sqlite3.connect(db_path)
rate_row = conn.execute(
"SELECT rate FROM billing_rates WHERE attorney = ? AND task_type = ?",
(attorney, task_type)
).fetchone()
rate = rate_row[0] if rate_row else 350.00
clean_description = improve_billing_description(raw_description, matter)
amount = round(hours * rate, 2)
conn.execute("""
INSERT INTO time_entries (attorney, client_id, matter, description, hours, rate, amount, date, created_at)
VALUES (?,?,?,?,?,?,?,?,?)
""", (attorney, client_id, matter, clean_description, hours, rate, amount,
datetime.utcnow().strftime("%Y-%m-%d"), datetime.utcnow().isoformat()))
conn.commit()
conn.close()
return {"description": clean_description, "hours": hours, "rate": rate, "amount": amount}
Step 3: Improve Billing Descriptions with AI
import anthropic
from dotenv import load_dotenv
load_dotenv()
def improve_billing_description(raw_description, matter):
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=200,
system="""Rewrite this time entry description for a legal invoice.
Rules:
- Start with a verb (Drafted, Reviewed, Conducted, Prepared, Attended, etc.)
- Be specific about what was done
- Include who was involved if mentioned
- Keep under 50 words
- Professional tone
- No vague language like "worked on" or "various"
- Do not add information not in the original""",
messages=[{
"role": "user",
"content": f"Matter: {matter}\nRaw entry: {raw_description}"
}]
)
return response.content[0].text.strip()
Step 4: Generate Monthly Invoices
def generate_invoice(client_id, client_name, period_start, period_end, db_path="legal_billing.db"):
conn = sqlite3.connect(db_path)
entries = conn.execute("""
SELECT id, attorney, matter, description, hours, rate, amount, date
FROM time_entries
WHERE client_id = ? AND status = 'unbilled' AND billable = 1
AND date BETWEEN ? AND ?
ORDER BY date, attorney
""", (client_id, period_start, period_end)).fetchall()
if not entries:
conn.close()
return None
total_hours = sum(e[4] for e in entries)
total_amount = sum(e[6] for e in entries)
invoice_number = f"INV-{datetime.utcnow().strftime('%Y%m')}-{client_id:04d}"
conn.execute("""
INSERT INTO invoices (client_id, client_name, invoice_number, total_hours, total_amount,
period_start, period_end, created_at)
VALUES (?,?,?,?,?,?,?,?)
""", (client_id, client_name, invoice_number, total_hours, total_amount,
period_start, period_end, datetime.utcnow().isoformat()))
entry_ids = [e[0] for e in entries]
placeholders = ",".join(["?"] * len(entry_ids))
conn.execute(f"UPDATE time_entries SET status = 'billed' WHERE id IN ({placeholders})", entry_ids)
conn.commit()
conn.close()
return {
"invoice_number": invoice_number,
"client": client_name,
"entries": [{"attorney": e[1], "matter": e[2], "desc": e[3], "hours": e[4], "rate": e[5], "amount": e[6], "date": e[7]} for e in entries],
"total_hours": total_hours,
"total_amount": total_amount
}
Step 5: Format the Invoice Output
def format_invoice_text(invoice):
lines = [
f"INVOICE: {invoice['invoice_number']}",
f"Client: {invoice['client']}",
f"Total: ${invoice['total_amount']:,.2f} ({invoice['total_hours']:.1f} hours)",
"",
f"{'Date':<12} {'Attorney':<15} {'Description':<45} {'Hours':>6} {'Rate':>8} {'Amount':>10}",
"-" * 96
]
for e in invoice["entries"]:
desc = e["desc"][:43] if len(e["desc"]) > 43 else e["desc"]
lines.append(f"{e['date']:<12} {e['attorney']:<15} {desc:<45} {e['hours']:>6.1f} ${e['rate']:>7.0f} ${e['amount']:>9.2f}")
lines.append("-" * 96)
lines.append(f"{'TOTAL':>74} {invoice['total_hours']:>6.1f} {'':>8} ${invoice['total_amount']:>9.2f}")
return "\n".join(lines)
if __name__ == "__main__":
invoice = generate_invoice(1, "Acme Corp", "2025-11-01", "2025-11-30")
if invoice:
print(format_invoice_text(invoice))
What to Build Next
Add unbilled time alerts. If an attorney has logged zero hours for a client in 7+ days but has an active matter, send a reminder. That catches time entries that are being forgotten.
Related Reading
- Implementing Automated Billing and Invoicing - billing automation patterns
- AI in Legal and Compliance - AI in legal practice
- Building a Reporting Dashboard from Scratch - billing dashboard design
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment