Part 1 of 7 · Recurring invoice generator series ~10 min read

A recurring invoice generator on AWS for a few dollars a month

A business that runs on retainers bills the same customers, the same amounts, on the same dates, month after month — and that very regularity is what makes it slip. The invoice that goes out three days late because someone was on holiday; the one still on last quarter’s figure because the price rose and nobody updated the template; the mid-month upgrade that never made it onto a bill at all. This post walks through the design of a small generator that turns each contract into a schedule and bills it, to the penny, on the day it’s due.

Key takeaways

  • Each contract or retainer defines its line items, its cycle — monthly, quarterly, or per-milestone — and the customer’s jurisdiction.
  • Every contract gets its own EventBridge Scheduler entry that fires on the due date and builds that invoice; nothing runs in between.
  • Proration, tax, and totals are plain, deterministic code. No model ever touches a number on the invoice.
  • A built invoice is rendered to a PDF, emailed through SES, and written to a ledger — every figure auditable.
  • Designed on AWS for about $2.00/month at roughly 120 invoices a month. Overdue invoices are handed to a separate chaser, never nagged from here.

The whole system on one page

Before any code, here’s the shape of what we’re designing. A business that runs on retainers and contracts bills the same customers, the same amounts, on the same dates, month after month. That regularity ought to make it the easiest job in the office — and yet it’s the one that slips. The invoice that goes out three days late because the person who sends it was away. The one still showing last quarter’s price because the rate rose and the template didn’t. The mid-month upgrade that never reached a bill because nobody did the half-month maths. The system below does exactly one thing: it bills each contract, to the penny, on the day it’s due — and then gets out of the way.

System architecture: contracts and rules in, four pieces inside AWS, invoices out to customers and overdue ones to a separate chaser At the top, three external boxes in a row. Far left, “Contracts & retainers” — a Google Sheet with one row per contract holding the customer, the line items, the cycle (monthly, quarterly, or per-milestone), the anchor date, the term, and the customer’s jurisdiction. Centre, “Tax & numbering rules” — a short settings doc with the tax rate per jurisdiction, the invoice-numbering format, and the sender details. Far right, “Customers” — where each finished invoice PDF is emailed. Each connects by an arrow to the AWS account container below. Contracts and rules feed in to ground every invoice; customers receive the sent PDF. Inside the AWS account are four components in a row. Left, the Scheduler — EventBridge Scheduler holds one entry per contract and fires on each due date. Next, the Builder — pulls the contract’s line items, applies proration for any mid-cycle change and the right tax for the jurisdiction, assigns the next invoice number, and computes the totals, all in plain code. Next, Render — turns the built invoice into a clean PDF stored in a versioned S3 bucket. Next, Send & record — emails the PDF through SES and writes the invoice to the ledger. Below the row sits a Ledger store (DynamoDB) holding every invoice and its exact workings, and a daily Sweep that re-checks due and unpaid invoices. An arrow leaves the sweep to an external box, “Invoice chaser”, marked as a separate system: anything overdue is handed to it through a queue rather than chased here. A note at the bottom reads: the generator only ever builds and sends scheduled invoices — the arithmetic is plain code, and overdue invoices go to a separate chaser. Contracts & retainers line items, cycle, jurisdiction Tax & numbering rules rates, format, sender Customers where invoices land grounds rules invoice PDF out AWS account Scheduler one entry per contract, fires on due Builder proration, tax, totals — plain code Render HTML → PDF, versioned in S3 Send & record email via SES, write the ledger Ledger every invoice + workings Daily sweep finds overdue Invoice chaser separate system hand-off The arithmetic is plain code — no model touches the money.
Fig 1. Contracts and rules outside, four pieces inside AWS. A per-contract scheduler fires on each due date; the builder does the arithmetic in plain code; render makes the PDF; send emails it and writes the ledger. Overdue invoices are handed to a separate chaser.

What you set up once (the outside)

  • Contracts and retainers. A Google Sheet in a Drive folder with one row per contract: the customer, their email, the line items (a description, a quantity, and a unit price for each), the billing cycle (monthly, quarterly, or per-milestone), the anchor date the cycle counts from, the term, and the customer’s tax jurisdiction. This is the same list a finance person already keeps in their head or in a spreadsheet; here it’s in one place the generator can read. A small sync mirrors it into AWS so the schedules and the builder always work from the current version. How a row becomes a live schedule is Part 2.
  • Tax and numbering rules. One short settings doc in the same folder. It holds the tax rate per jurisdiction (UK VAT 20%, Ireland 23%, a zero-rated state, and so on), the invoice-numbering format (a prefix, a running sequence, the reset rule), your sender name and address for the PDF, and the payment terms. Changing a VAT rate or the numbering format is an edit to this doc, not a deploy.
  • Customers. The people who receive the invoices. Each finished invoice goes out as a PDF attached to a plain, templated email from your verified sending address, with a short covering line and a link to view it online. The generator only ever sends the invoice — it never takes payment, and it never follows up. That second job belongs to a separate chaser, by design.

What runs each cycle (the inside)

  • The scheduler. Every contract gets its own EventBridge Scheduler entry, set to fire on its next due date according to its cycle. A monthly retainer anchored to the 1st fires on the 1st; a quarterly contract fires every three months from its anchor; a per-milestone contract fires when its milestone is marked done. Nothing polls and nothing runs between cycles — the schedule simply wakes the builder on the right day. This is Part 2.
  • The builder. When a schedule fires, the builder loads the contract, pulls its line items, applies proration for any change that happened part-way through the cycle, looks up the tax rate for the customer’s jurisdiction, computes every line total and the invoice total, and assigns the next number in sequence. All of this is plain Python — the same inputs always produce the same invoice. This is Parts 3 and 4.
  • Render and send. The built invoice is poured into an HTML template and rendered to a PDF inside Lambda, then written to a versioned S3 bucket so every issued document is kept exactly as it was sent. SES emails it to the customer, and the invoice — with its full workings — is written to the ledger in DynamoDB. This is Part 5.
  • The sweep and the hand-off. A daily sweep re-checks invoices that are due and still unpaid. It doesn’t nag anyone. It marks each overdue invoice in the ledger and pushes it onto a queue for a separate invoice chaser, with the invoice id, customer, amount, and due date attached. Keeping generation and chasing apart keeps this system a clean, predictable biller. Also in Part 5.

In plain words

It’s the 1st of July. A design studio bills a client on a £1,500/month retainer, anchored to the 1st. On 16 June that client added a second service line worth £600/month. At one minute past midnight the contract’s schedule fires. The builder loads the contract, sees the base retainer (£1,500 for July), the new line (£600 for July), and a proration catch-up for the half-month the new line ran in June — £600 × 15/30 = £300. That’s a net subtotal of £2,400. The client is in the UK, so VAT at 20% adds £480, for a total of £2,880. The builder assigns invoice INV-2026-0148, renders the PDF, and SES emails it before anyone is awake. The ledger now holds the invoice, the line-by-line workings, and the exact proration sum — so when the client queries it, the answer is right there.

Three weeks later that invoice is still unpaid. The generator does nothing dramatic. The daily sweep notices it has passed its due date, marks it overdue in the ledger, and drops a small message onto the chaser’s queue. The chaser — a separate system with its own tone and escalation rules — takes it from there. The generator is already on to next month.

Design rules that shaped every decision

  • One job. The generator builds and sends scheduled invoices — it does not chase, quote, or take payment.
  • No model touches the money. Every figure is plain, deterministic code that produces the same invoice every time.
  • One schedule per contract. Each contract’s cycle is its own EventBridge entry; nothing polls and nothing runs between cycles.
  • Every invoice is kept exactly as sent. The PDF is versioned in S3 and the workings are written to the ledger.
  • Rules live in a doc. Tax rates, the numbering format, and the sender details change without a deploy.
  • Overdue is someone else’s job. Anything unpaid is handed cleanly to a separate chaser, never nagged from here.

Why this shape

Most small teams run recurring billing one of three ways: a person re-uses last month’s invoice and edits the date, the accounting package fires a fixed template that can’t handle a mid-cycle change, or it’s all in someone’s head and goes out when they remember. Editing last month’s copy is how a stale price survives for a year. A rigid template is how a half-month upgrade gets billed as nothing or as a whole month. And “when someone remembers” is how cash flow develops a stutter. None of these keep a clean record of why a number is what it is — which is exactly what you want the day a client disputes a line.

The shape above keeps the contract you already have as the single source of truth, turns it into a schedule that fires on the right day on its own, and does the arithmetic the same boring way every time — in code you can read, with the workings saved. The 95% of invoices that are simply “this month, same as last” go out untouched and on time. The few with a mid-cycle change are prorated correctly to the penny. And because the generator refuses to also be a chaser, it stays small enough to trust.

The next four posts walk through each piece in turn: how a billing schedule gets set up, how an invoice gets built, how proration and tax get applied (with the numbers), and how an invoice gets sent and handed off. One diagram per post. A cost breakdown and a final engineering reference at the end.

All posts