How to Automate Payment Reminders That Run Themselves
How to automate payment and renewal reminders: a scheduled job, rules stored as data, a unique constraint so each one sends once, and an alert if the job stops.
To automate payment reminders, run a scheduled job that finds every invoice or plan due for a reminder, claims each send with a unique database row so overlapping or repeated runs never send it twice, delivers it through the customer's channel, and records the result. Keep timing and wording as data. Then alert someone if the job itself stops running.
TL;DR: A reminder system is a schedule, a query, a send, and a record. The record does the hard work: a unique constraint on invoice, reminder type, and due date stops overlapping runs from sending twice. Check the invoice is still unpaid just before sending, cap the sequence, and alert when a run goes missing.
Payment and renewal reminders are the ones that pay for themselves, because every missed one is money you've already earned and haven't collected. In GymFast, our gym management platform, renewal, payment, class, and session reminders run on a schedule as scheduled edge functions, so staff stop chasing. This post focuses on the part most reminder systems get wrong: sending each reminder once, and only once.
Which reminders should you automate first?
Start with the one that loses the most money when it's forgotten. For most businesses that's payment due and plan renewal, because each missed reminder is revenue already earned and not yet collected. Appointment reminders and enquiry follow-ups come next.
| Reminder | Trigger | Why it pays |
|---|---|---|
| Payment due | An invoice is unpaid near or past its due date | Cash arrives without awkward calls |
| Renewal | A plan or contract ends soon | Keeps recurring revenue |
| Expiry | A plan has ended | Tells the customer and updates their status |
| Appointment or class | A booking is coming up | Fewer no-shows |
| Enquiry follow-up | A lead has had no reply | Leads don't go cold |
For gyms specifically, our gym software guide covers which reminders a gym needs alongside everything else. Enquiry follow-ups are a sales problem as much as a reminder problem; our post on getting website enquiries into a CRM covers that side.
The four parts of a reminder system
Whatever the stack, there are four parts: a schedule that runs the job, a query that finds what's due, a send step, and a record of each send. The schedule and the send are the easy parts. The record is what stops a customer getting the same payment reminder four times.
- A schedule: every few minutes, hourly, or daily.
- A query that selects invoices or plans due for a reminder now.
- A send through email, SMS, WhatsApp, or push.
- A record of what was sent, for which date, and whether it worked.
On Supabase, the documented way to run an edge function on a schedule is the pg_cron extension combined with pg_net, which calls the function over HTTP, with the auth token kept in Supabase Vault rather than in the job definition (Supabase docs). Other stacks use a cron service, a cloud scheduler, or a job queue with delayed jobs. We cover Supabase work on the Supabase development page.
Why store reminder rules as data?
Because owners change their minds, and they should be able to. "Send it five days before instead of three." "Add a second one on the due date." If those rules live in code, each change waits for a developer; if they live in a table, staff edit them in an admin screen and the next run uses them.
A rule needs only a few fields: the reminder type, when it fires relative to a date (days before or after), the channel, a message template with placeholders for name, date, and amount, and whether it's switched on. The job reads the rules and applies them. Simple, and it means wording changes never need a deploy.
How do you make sure a reminder sends only once?
Claim the send in the database before sending, using a unique constraint on the invoice, the reminder type, and the due date. Jobs overlap, retry after failures, and rerun after deploys. With the constraint in place, only one run can insert the claim; every other run gets nothing back and skips that reminder.
Here's the whole idea in Postgres:
create table reminder_sends (
id bigserial primary key,
invoice_id bigint not null references invoices(id),
kind text not null, -- 'due_soon', 'due_today', 'overdue_3d'
due_date date not null,
sent_at timestamptz,
error text,
unique (invoice_id, kind, due_date)
);
-- Claim the send. No row returned means another run already claimed it.
insert into reminder_sends (invoice_id, kind, due_date)
values ($1, $2, $3)
on conflict (invoice_id, kind, due_date) do nothing
returning id;
ON CONFLICT DO NOTHING simply skips the insert when the row already exists (PostgreSQL docs). After a successful send, set sent_at. If the send fails, store the error and let a retry pass pick up claimed rows with no sent_at. One gap remains: a crash after the provider accepts the message but before sent_at is written means the retry sends it again. Where the provider accepts an idempotency key, pass the claim row's id to close that gap. Including due_date in the key matters: next month's invoice is a new reminder, not a duplicate.
What if two copies of the job start at once? The constraint already prevents double sends, but running twice wastes work. A transaction-level advisory lock, such as pg_try_advisory_xact_lock, lets the second run see the lock is taken and exit; the PostgreSQL docs explain that session-level advisory locks survive a rollback, so the transaction-level variant is the safer choice here (PostgreSQL docs). For heavier workloads, our Postgres job queue post covers leases and SKIP LOCKED.
How do you stop a reminder that's no longer needed?
Check the invoice's current state just before sending, not only when the job started. If the customer paid this morning, the afternoon reminder must not go out. A paid invoice that still gets "your payment is overdue" costs more goodwill than the reminder was ever worth.
Good automations change state as well as send messages. GymFast's schedule also handles plan expiry and auto check-out, so a member's status is accurate without staff doing anything. For payments, that might mean marking an invoice overdue and adding it to a call list after the last reminder. Cap the sequence at two or three reminders, then hand it to a person.
Write each message for one purpose: "Your invoice of ₹2,400 is due on 14 March. Pay here." Include the date, the amount, and a link or number, in the business's name and voice, not the software vendor's.
Which channel should payment reminders use?
Email suits invoices and receipts and is the easiest start. SMS and WhatsApp get read faster, which suits short nudges. Push notifications reach app users at no per-message cost. Whatever you choose, let customers pick their channel and honour opt-outs.
WhatsApp has rules of its own. Template messages are the only type a business can send to a WhatsApp user outside a customer service window, and a template must have APPROVED status before it can be sent (Meta for Developers). Meta changes these rules from time to time, so check the current docs and plan template review into the timeline.
Time zones matter too. Store dates in UTC and send in the customer's local time, inside a sending window per channel. A payment reminder at 3 am gets noticed for the wrong reason.
How do you know when the reminder job stops?
Record every run and alert on the gap. A reminder job that silently stops is worse than none, because everyone believes it's working. Payments go unchased for weeks before anyone asks why the cash came in late.
create table job_runs (
job text not null,
started_at timestamptz not null default now(),
finished_at timestamptz,
sent_count int
);
-- Run from a separate checker: any job with no finished run in two hours.
select job, max(finished_at) as last_ok
from job_runs
group by job
having max(finished_at) < now() - interval '2 hours';
Alert on three things: a run that didn't happen when expected, a run that sent zero reminders on a day it normally sends many (or far more than usual), and failed sends piling up. Run the checker from somewhere other than the job it watches, or one outage silences both.
Test reminders before going live
Seed invoices due today, tomorrow, and last week, then run the job and check what went out. Run it twice in a row and confirm nothing sends twice. Pay one invoice between runs and confirm its reminder stops. Change a rule and confirm the next run uses it. Finally, stop the job and confirm the alert fires.
If your reminders currently live in a spreadsheet and someone's memory, our post on turning a spreadsheet into an app covers that first step.
FAQ
How many payment reminders should a customer get?
Two or three per invoice is usually enough: one shortly before the due date, one on the day, and one a few days after. After that, a person should call. More automated messages tend to get ignored or unsubscribed from, and they make a late payment feel like a dispute rather than an oversight.
Can a payment reminder include a link to pay?
Yes, and it should. A link straight to the invoice or a payment page removes the step where a customer has to find the bill. Generate the link per invoice, make it expire sensibly, and check the invoice is still unpaid when the link is opened, not only when the reminder was sent.
Should reminders stop when a customer replies?
For follow-ups and overdue payments, usually yes. A reply means a person is now in the conversation, so pause the sequence and flag it for staff. For routine reminders, such as an upcoming class, a reply needn't stop anything. Store the pause as a state on the record, so the job respects it.
Losing money to reminders nobody sends? See our workflow automation work, or our membership management software page for renewals, plans, and payments.