Reminder API: Schedule Notifications for Later

Schedule reminders and notifications per user with timezone-aware timing, retries, and failure alerting. Works with any email, SMS, or push provider. Your handler decides what to send — Posthook handles when.

Last updated: March 24, 2026

Trial expiry warnings, appointment reminders, onboarding nudges, payment due date alerts — most SaaS applications need to send users something at a specific future time. The scheduling part is deceptively complex: per-user timers, timezone handling, cancellation when the action is no longer needed, retries when delivery fails, and visibility when things go wrong.

There are three categories of tools that solve this. Customer engagement platforms (Customer.io, Braze, OneSignal) own the full notification lifecycle — templates, channel routing, engagement analytics. Timing layers handle when to trigger your application and let you handle the notification logic. DIY approaches (cron + database, queue with delay) give you full control at the cost of building and maintaining the scheduling infrastructure yourself. Posthook is a timing layer: it handles the scheduling, delivery, retries, and observability. Your application handles what to send and how.

Common reminder use cases

  • Trial expiration reminders — send warnings at 7 days, 3 days, 1 day, and 1 hour before trial ends. Each user has their own trial end date. The developer needs per-user timers created at signup.
  • Appointment reminders — remind at 3 days, 1 day, and 1 hour before. Must be in the user’s local timezone. If the appointment is rescheduled, old reminders need to be replaced with new ones.
  • Onboarding follow-ups — nudge users who signed up but have not completed a key step after 48 hours. If they complete it before the timer fires, the reminder should be a no-op.
  • Payment due date reminders — send reminders before subscription renewal or invoice due date. A critical payment reminder may need a more aggressive retry strategy than a marketing nudge.
  • Invitation follow-ups — remind invitees who have not accepted after a few days. If they accept before the reminder fires, skip it.

These all share the same core model: schedule a timer when the triggering event happens, and when it fires, check whether the action is still needed.

Why reminders become infrastructure work

Reminders start simple and accumulate complexity:

  1. A cron job scans for due reminders. Works at low volume.
  2. Each user gets their own timer. The database scan grows with every signup.
  3. Timezones appear. Naive UTC offset conversion breaks at DST transitions — reminders fire an hour early or an hour late twice a year.
  4. Cancellation logic arrives. “Do not send if the user already acted” needs state tracking and race-condition handling in a batch processor.
  5. Multi-step sequences follow. Three timers per entity, each with its own cancellation path.
  6. The email API goes down. Reminders are lost unless you built retry logic with attempt tracking and backoff.
  7. Nothing alerts you when reminders stop working. Users complain before monitoring does.

Each step adds infrastructure — a polling query, a retry mechanism, a dead-letter path, a monitoring check. The reminders and follow-ups pattern covers this progression in depth.

Notification platforms vs timing layers

Developers searching for “reminder API” often land on two very different categories of tools. Knowing the distinction helps you pick the right one.

NeedNotification platformTiming layer (Posthook)
Template editing without deploysYesNo — templates live in your code
Multi-channel routing (email + push + SMS)YesNo — you choose the channel in your handler
Engagement analytics (opens, clicks)YesNo — use your analytics tool
Arbitrary application logic at fire timeLimited to platform actionsYes — your handler runs whatever you need
Works with existing email/SMS providerReplaces or wraps itComplements it
Per-delivery observabilityVaries by platformYes — every delivery inspectable with attempt history
Failure alertingVaries by platformAnomaly detection per endpoint, alerts via email/Slack/webhook
Timezone scheduling with DSTSome platforms support itFirst-class — postAtLocal + IANA timezone
Team ownershipMarketing/product-managedDeveloper-managed

Choose a notification platform when the team includes marketing or product people who edit templates without deploys, when engagement analytics are a core requirement, or when the company is ready to adopt a customer data platform model.

Choose a timing layer when the developer controls notification logic in their own codebase, when the “reminder” triggers application logic beyond sending a template, or when the team already uses an email/SMS/push provider and just needs reliable timing.

How Posthook handles reminder scheduling

The model is straightforward:

  1. A triggering event happens in your application (user signs up, appointment created, invoice generated).
  2. Your application schedules a hook via the Posthook API with a delivery time and a payload.
  3. Posthook persists the schedule and handles the timing.
  4. At the scheduled time, Posthook delivers an HTTP POST to your endpoint.
  5. Your handler checks current state and decides: send the reminder, or skip it.
  6. If your endpoint fails, Posthook retries with configurable backoff.
  7. If your endpoint starts failing consistently, anomaly detection alerts you.

Schedule a reminder for 48 hours after signup:

import Posthook from '@posthook/node';

const posthook = new Posthook('phk_...');

const hook = await posthook.hooks.schedule({
  path: '/webhooks/onboarding-reminder',
  postIn: '48h',
  data: { userId: 'user_123', step: 'complete-profile' },
});

// Store hook.id if you want to cancel later
await db.updateUser('user_123', { reminderHookId: hook.id });

When the hook fires, your handler checks state and decides:

// POST /webhooks/onboarding-reminder
export async function handler(req, res) {
  const { userId, step } = req.body.data;
  const user = await db.getUser(userId);

  if (user.profileCompleted) {
    // Already done — no-op
    return res.status(200).json({ status: 'skipped' });
  }

  await sendOnboardingReminder(user);
  return res.status(200).json({ status: 'sent' });
}

The handler always checks state before acting. If the user completed onboarding before the reminder fired, the delivery resolves as a no-op. This is the expected happy path. You can also cancel the hook early with posthook.hooks.delete(hook.id) to avoid the unnecessary delivery, but the handler is safe to run regardless.

Timezone-aware reminders

Timezone handling is the most common pain point in reminder scheduling. “Send at 9am in the user’s timezone” sounds simple until DST transitions cause reminders to fire at 8am or 10am twice a year.

Posthook handles this with postAtLocal and an IANA timezone. You provide the local time the user should receive the reminder, and Posthook resolves the correct UTC delivery time — including through DST transitions.

// Appointment reminder at 9am in the patient's timezone, one day before
const hook = await posthook.hooks.schedule({
  path: '/webhooks/appointment-reminder',
  postAtLocal: '2026-11-01T09:00:00',
  timezone: 'America/Chicago',
  data: {
    appointmentId: 'apt_456',
    patientId: 'patient_789',
    reminderType: '1-day',
  },
});

No manual UTC offset conversion. No DST lookup tables. The reminder fires at 9am Central whether that is UTC-5 or UTC-6.

If the appointment is rescheduled, cancel the old reminders and schedule new ones:

// Appointment rescheduled — cancel old reminders, schedule new ones
await posthook.hooks.delete(existingReminder1DayId);
await posthook.hooks.delete(existingReminder1HourId);

const newReminder = await posthook.hooks.schedule({
  path: '/webhooks/appointment-reminder',
  postAtLocal: '2026-11-15T09:00:00',
  timezone: 'America/Chicago',
  data: { appointmentId: 'apt_456', patientId: 'patient_789', reminderType: '1-day' },
});

Even if cancellation fails or a race condition means the old hook still fires, the handler checks whether the appointment time matches and skips if it does not.

Multi-step reminder sequences

Many reminder use cases involve a sequence — 3 days before, 1 day before, 1 hour before. There are two approaches:

Schedule all reminders at the triggering event. When an appointment is created, calculate the reminder times relative to the appointment and schedule all three hooks immediately. Each fires independently at its scheduled time. If the appointment is cancelled, delete all three. This is the simplest model and works well when the sequence is known upfront.

// Appointment is March 20 at 2pm — schedule reminders before it
const appointmentAt = new Date('2026-03-20T14:00:00Z');

const reminders = [
  { daysBeforeMs: 3 * 86400000, type: '3-day' },
  { daysBeforeMs: 1 * 86400000, type: '1-day' },
  { daysBeforeMs: 1 * 3600000,  type: '1-hour' },
];

const hooks = await Promise.all(
  reminders.map(({ daysBeforeMs, type }) =>
    posthook.hooks.schedule({
      path: '/webhooks/appointment-reminder',
      postAt: new Date(appointmentAt.getTime() - daysBeforeMs).toISOString(),
      data: { appointmentId: 'apt_456', reminderType: type },
    })
  )
);

Chain reminders from the handler. The first reminder fires, and the handler schedules the next one. This gives you more flexibility — the handler can decide whether to continue the sequence based on current state — but adds coupling between steps.

Both approaches work. The first is easier to reason about. The second is more flexible. For recurring multi-step workflows, Posthook sequences provide calendar scheduling with dependency graphs and config-as-code.

When not to use Posthook for reminders

You need a template editor and engagement analytics. If the team includes marketing or product people who edit notification templates without code deploys, and you need open rates, click tracking, and A/B testing, use a customer engagement platform. Posthook does not send notifications — it triggers your application to send them.

Your reminders are simple batch digests. If the job is “send all daily summary emails at 8am,” a cron job that queries for eligible users is simpler. Posthook fits when each user, each event, or each entity creates its own timer with its own deadline.

You need complex journey orchestration. If the logic involves branching — “if the user opened email A, send B; otherwise send C” — that is a workflow problem. Consider a workflow platform like Inngest or Trigger.dev. Posthook handles the timing layer, not the branching logic.

Frequently asked questions

Ready to get started?

Create your free account and start scheduling hooks in minutes. No credit card required.