Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

What WhatsApp's Cloud API forces you to design before you send a single reminder

I thought the hard part of a reminder app would be the user interface. Turns out the hard part is everything that needs to happen before the first reminder can even get sent out. Why I'm building this People

I thought the hard part of a reminder app would be the user interface. Turns out the hard part is everything that needs to happen before the first reminder can even get sent out.

Why I'm building this

People don't forget to pay for their subscriptions. They forget to cancel them. The idea was to have the reminder show up in the right place, a couple days in advance, when I can still cancel the payment.

It turned into RenewChime: a WhatsApp-first subscription tracker. Default currency is PKR and everything is managed via a chat.

What I built

  • Next.js (App Router), TypeScript, Tailwind
  • Supabase for Auth, Postgres and Row Level Security
  • Subscriptions CRUD, dashboard, analytics and reminders screen
  • Rule-based chat bot (add Netflix 1500 PKR monthly 5th, list, upcoming, paid Netflix, snooze Netflix 2d)
  • Cron route that finds due reminders and sends them

Status, honestly: the whole flow works end to end on a mock provider. The Cloud API provider is written behind the same interface but isn't tested against Meta yet. I'm still getting a test number and templates approved.

How it's structured

1. One provider interface. The app never talks to WhatsApp directly.

interface WhatsAppProvider {
  sendTemplate(to: string, template: string, params: string[]): Promise<void>;
  sendText(to: string, text: string): Promise<void>;
  verifySignature(rawBody: string, headers: Headers): boolean;
}

A mock implementation is the default. The real one is selected with an env var. That's what let me build the whole product before Meta approved anything.

2. Consent is data, not a checkbox. whatsapp_contacts stores opted_in_at, opted_out_at and last_inbound_at. The cron skips any contact without a recorded opt-in, and STOP sets opted_out_at immediately.

3. Templates vs free text. A business can't start a conversation with free text. Reminders go out as pre-approved templates:

Hi {{1}}, your {{2}} subscription ({{3}} {{4}}) renews on {{5}}.
Reply PAID, SNOOZE or CANCEL.

Free-form replies are only allowed within 24 hours of last_inbound_at. This also bit me with verification codes: a number that has never messaged you has no open window.

4. Claim before you send. Two overlapping cron runs shouldn't double-send. Instead of check-then-send, each run claims a row atomically (simplified):

update reminders
set claimed_at = now()
where id = $1 and status = 'queued' and claimed_at is null
returning id;

Only the run that gets a row back sends. Failed sends retry after 5 and 15 minutes, then are marked failed.

5. The webhook trusts nothing. It verifies X-Hub-Signature-256 against the raw body with a timing-safe compare, and dedupes inbound messages by provider message ID.

What I'd have shipped without noticing

  • Month-end renewals: a subscription on Jan 31 has to roll to Feb 28, not March 3.
  • Money stored as integer minor units, converted only for display.
  • Quiet hours and timezones: reminders outside 9am to 9pm local time move to the next window.

Result

Working today: add, pause, cancel and mark-paid via UI or chat, reminders scheduled at 7, 3 and 1 day, atomic sending, and Row Level Security tested with two users. Next: real delivery through the Cloud API.

Live: https://renewchime.vercel.app
GitHub: comment "Chime" I'll send it to you

Final thoughts

The platform's constraints turned out to be product design in disguise. Opt-in, templates and the 24-hour window are annoying to build around, but they're exactly what keeps a reminder from feeling like spam.

Should I break down the reminder engine in a follow-up? Want the provider layer as a small open-source package? And what should I build next?

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.