Reminder API: Schedule Notifications for Later

Last updated: April 23, 2026

You need to remind a user about something at a specific future time. A trial that ends in three days. An appointment tomorrow morning. An invitation that expires Friday. A nudge for the user who started onboarding but never finished. The scheduling sounds simple until you account for per-user scheduling, timezone math, cancellation when the action is no longer needed, retries when delivery fails, and visibility when things go quiet.

Posthook is a webhook scheduler. You schedule a hook when the triggering event happens. At the scheduled time, Posthook fires an HTTP POST to your endpoint. Your handler decides what to send and how. You keep your notification logic, templates, and providers (SendGrid, Twilio, FCM, whatever you already use). Posthook handles the timing and the operational layer around it.

Common reminder use cases

  • Trial expiration: warn at 7, 3, and 1 days before the trial ends. Each user has their own deadline.
  • Appointment reminders: nudge a customer the day before and an hour before, in their local time.
  • Onboarding follow-ups: ping users who signed up but haven’t completed a key step after 48 hours. If they finish before the hook fires, the reminder is a no-op.
  • Payment and invoice reminders: send dunning notices before subscription renewal, after a failed charge, or as the invoice due date approaches.
  • Invitation follow-ups: remind invitees who haven’t accepted after a few days. If they accept first, skip.
  • Support case follow-ups: nudge the assignee if a ticket isn’t resolved in 48 hours. Escalate after another 24.
  • Timeline-based follow-up alerts: notify on milestones, deadlines, and stale tasks across multi-step processes (project workflows, healthcare protocols, loan application stages).
  • Compliance and safety reminders: scheduled across timezones with delivery records of whether each reminder fired.

Each of these is one scheduled action per user, per event, or per record. Not a recurring cron cadence. The scheduling is dynamic, triggered by something that just happened in your application.

Notification platforms vs webhook schedulers

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

NeedNotification platformWebhook scheduler (Posthook)
Template editing without deploysYesTemplates live in your code
Multi-channel routing (email, push, SMS)YesYou pick the channel in your handler
Engagement analytics (opens, clicks)YesUse your analytics tool
Arbitrary application logic at fire timeLimited to the platform’s action setYour handler runs whatever you need
Works with your existing providerOften replaces or wraps itCalls whatever provider your handler already uses
Per-delivery observabilityVaries by platformPer-hook delivery status with attempt history
Failure alertingVaries by platformPer-endpoint anomaly detection, alerts via email, Slack, or webhook
Timezone scheduling with DSTSome platforms support itpostAtLocal + IANA timezone with DST handled at scheduling time
Team ownershipMarketing or productDevelopers

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 webhook scheduler when developers control 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, or push provider and just needs reliable scheduling with visibility.

How Posthook handles reminder scheduling

Seven steps:

  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 identifier.
  3. Posthook persists the schedule and handles the timing.
  4. At the scheduled time, Posthook delivers an HTTP POST or WebSocket message to your endpoint.
  5. Your handler reads the identifier, looks up current state, and decides: send the reminder, or skip it.
  6. If your endpoint fails, Posthook retries with configurable backoff.
  7. If delivery outcomes deviate from your endpoint’s baseline, anomaly detection alerts you. A single failure on a normally-healthy endpoint is enough.
flowchart LR
A[Triggering event<br/>signup, booking, invoice]
B[Posthook<br/>persists schedule]
C[Handler<br/>reads identifier<br/>looks up state]
D[Send reminder]
E[Skip]
A -->|schedule hook| B
B -->|fires HTTP POST<br/>at scheduled time| C
C -->|state relevant| D
C -->|state changed| E

classDef default fill:#111113,stroke:#27272a,stroke-width:1px,color:#fafafa,rx:6,ry:6
classDef accent fill:#111113,stroke:#7CFEF0,stroke-width:1.5px,color:#fafafa,rx:6,ry:6
classDef outcome fill:#0a0a0b,stroke:#3f3f46,stroke-width:1px,color:#a1a1aa,rx:6,ry:6
class C accent
class D,E outcome
Reminder lifecycle. The triggering event creates a scheduled hook. At fire time, the handler reads the identifier, checks state, and either acts or skips.

Schedule a reminder 48 hours after signup:

import Posthook from '@posthook/node';

const posthook = new Posthook('phk_...', { signingKey: 'phsk_...' });

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

await db.updateUser('user_123', { reminderHookId: hook.id });

When the hook fires, the handler verifies the signature, parses the delivery, and decides:

app.post('/webhooks/onboarding-reminder', async (req, res) => {
  const delivery = posthook.signatures.parseDelivery(req.body, req.headers);
  const user = await db.getUser(delivery.data.userId);

  if (user.profileCompleted) {
    return res.status(200).json({ status: 'skipped' });
  }

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

The handler uses posthook.signatures.parseDelivery from the SDK to verify the signature and parse the payload (configure express.raw({ type: '*/*' }) so the raw body is available). Then it checks state. If the user completed onboarding before the reminder fired, the delivery resolves as a no-op. Cancellation with posthook.hooks.delete(hookId) is a cleanup optimization to avoid unnecessary deliveries. The handler stays safe to run regardless.

Reminder scenarios with Posthook

Four common reminder shapes. The handler pattern stays the same across all of them: parse the delivery, look up state, act or skip.

Trial expiration sequence

Schedule three reminders at signup: 7 days before trial end, 1 day before, and 1 hour before. Each fires independently. If the user upgrades before a reminder fires, the handler skips. You can also cancel the pending hooks as a cleanup step.

Posthook complements your billing platform (Stripe, Chargebee, Lemon Squeezy). Your billing system tracks subscription state. Posthook triggers the pre-expiry reminders so you can prompt the user to upgrade before the platform downgrades them.

// At signup or trial start
const trialEndsAt = new Date('2026-05-01T00:00:00Z');

const offsets = [
  { ms: 7 * 86400000, type: '7-day' },
  { ms: 1 * 86400000, type: '1-day' },
  { ms: 1 * 3600000,  type: '1-hour' },
];

const hooks = await Promise.all(
  offsets.map(({ ms, type }) =>
    posthook.hooks.schedule({
      path: '/webhooks/trial-reminder',
      postAt: new Date(trialEndsAt.getTime() - ms).toISOString(),
      data: { userId: user.id, reminderType: type },
    })
  )
);

await db.user.update(user.id, { trialReminderHookIds: hooks.map(h => h.id) });
app.post('/webhooks/trial-reminder', async (req, res) => {
  const delivery = posthook.signatures.parseDelivery(req.body, req.headers);
  const { userId, reminderType } = delivery.data;
  const user = await db.getUser(userId);

  if (!user || user.subscribed || user.trialExtended) {
    return res.status(200).json({ status: 'skipped' });
  }

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

When the user upgrades early, optionally clean up:

async function onUpgrade(user) {
  await Promise.all(
    user.trialReminderHookIds.map(id =>
      posthook.hooks.delete(id).catch(() => {})
    )
  );
}

Appointment reminders with rescheduling

Reminders for the day before and an hour before, in the customer’s local time. Store the active hook IDs on the appointment so the handler can detect a rescheduled appointment whose old hook is still firing.

Posthook complements scheduling platforms like Calendly, Cal.com, and Acuity. They own the booking UX and calendar integrations. Posthook triggers the pre-appointment reminders at the times your reminder logic actually needs. Some platforms support reminders, but with template constraints. Per-customer timing logic stays in your handler.

async function scheduleAppointmentReminders(appointment) {
  const dayBefore = await posthook.hooks.schedule({
    path: '/webhooks/appointment-reminder',
    postAtLocal: subtractHours(appointment.startsAtLocal, 24),
    timezone: appointment.timezone,
    data: { appointmentId: appointment.id, reminderType: '1-day' },
  });

  const hourBefore = await posthook.hooks.schedule({
    path: '/webhooks/appointment-reminder',
    postAtLocal: subtractHours(appointment.startsAtLocal, 1),
    timezone: appointment.timezone,
    data: { appointmentId: appointment.id, reminderType: '1-hour' },
  });

  await db.appointment.update(appointment.id, {
    activeReminderHookIds: { '1-day': dayBefore.id, '1-hour': hourBefore.id },
  });

  return [dayBefore.id, hourBefore.id];
}
app.post('/webhooks/appointment-reminder', async (req, res) => {
  const delivery = posthook.signatures.parseDelivery(req.body, req.headers);
  const { appointmentId, reminderType } = delivery.data;
  const appointment = await db.getAppointment(appointmentId);

  if (!appointment || appointment.cancelled) {
    return res.status(200).json({ status: 'skipped' });
  }

  // Skip if this hook is no longer the active reminder for this slot
  // (the appointment was rescheduled and a new hook took its place)
  if (appointment.activeReminderHookIds?.[reminderType] !== delivery.hookId) {
    return res.status(200).json({ status: 'skipped-rescheduled' });
  }

  await sendAppointmentReminder(appointment, reminderType);
  return res.status(200).json({ status: 'sent' });
});

When the appointment is rescheduled, schedule new hooks (which updates activeReminderHookIds), then optionally cancel the old ones. If cancellation misses, the handler’s hook ID check skips them.

async function reschedule(appointment) {
  const oldHookIds = Object.values(appointment.activeReminderHookIds ?? {});

  await scheduleAppointmentReminders(appointment); // overwrites activeReminderHookIds

  await Promise.all(
    oldHookIds.map(id => posthook.hooks.delete(id).catch(() => {}))
  );
}

Onboarding nudge with conditional chain

After signup, schedule a 48-hour check. If the user hasn’t completed their profile, send the first nudge and schedule a second check 72 hours later. The chain advances only when state hasn’t changed.

// At signup
await posthook.hooks.schedule({
  path: '/webhooks/onboarding-check',
  postIn: '48h',
  data: { userId: user.id, step: 1 },
});
app.post('/webhooks/onboarding-check', async (req, res) => {
  const delivery = posthook.signatures.parseDelivery(req.body, req.headers);
  const { userId, step } = delivery.data;
  const user = await db.getUser(userId);

  if (user.profileCompleted) {
    return res.status(200).json({ status: 'skipped' });
  }

  await sendOnboardingNudge(user, step);

  if (step < 3) {
    await posthook.hooks.schedule({
      path: '/webhooks/onboarding-check',
      postIn: '72h',
      data: { userId, step: step + 1 },
    });
  }

  return res.status(200).json({
    status: 'sent',
    nextStep: step < 3 ? step + 1 : null,
  });
});

Support case follow-up with escalation

If a ticket isn’t resolved in 48 hours, nudge the assignee. If still open another 24 hours later, escalate to the team lead. Each step is its own hook. The next is scheduled only when the current one acted.

Posthook complements support platforms like Zendesk, Intercom, and Linear. They own the ticket model and the agent UI. Posthook triggers the time-based nudges and escalations that the platform’s built-in SLA rules don’t quite cover.

// At ticket creation
await posthook.hooks.schedule({
  path: '/webhooks/ticket-check',
  postIn: '48h',
  data: { ticketId: ticket.id, action: 'nudge' },
});
app.post('/webhooks/ticket-check', async (req, res) => {
  const delivery = posthook.signatures.parseDelivery(req.body, req.headers);
  const { ticketId, action } = delivery.data;
  const ticket = await db.getTicket(ticketId);

  if (!ticket || ticket.resolved) {
    return res.status(200).json({ status: 'skipped' });
  }

  if (action === 'nudge') {
    await nudgeAssignee(ticket);
    await posthook.hooks.schedule({
      path: '/webhooks/ticket-check',
      postIn: '24h',
      data: { ticketId, action: 'escalate' },
    });
  } else if (action === 'escalate') {
    await escalateToLead(ticket);
  }

  return res.status(200).json({ status: 'sent', action });
});

For recurring multi-step workflows on a calendar schedule (not per-user triggered), see Posthook sequences.

What you get beyond the scheduled call

Posthook adds the operational layer that matters when reminders are part of your production system:

  • Per-endpoint anomaly detection. Posthook tracks each endpoint’s success rate against a rolling baseline. A single failure on a normally-healthy endpoint is enough to trigger an alert by email, Slack, or webhook. The cause might be your email provider rate-limiting, a deploy that broke serialization, or the database being down. No threshold to set, no monitoring stack to maintain.
  • Bulk replay after a downstream outage. If SendGrid was down for an hour and 200 reminders failed during that window, bulk retry replays them by endpoint and time range in one API call. The recovery path most teams build only after they need it.
  • Per-hook delivery history. Every scheduled reminder has an attempt history: when it fired, which response came back, how long the endpoint took, which retry attempt succeeded. When a customer asks about a missed reminder, the hook’s delivery record has the answer.
  • Per-hook retry overrides. Critical reminders (payment retries, time-sensitive expirations) can take a more aggressive retry strategy without changing project defaults. Set retryOverride at scheduling time.
  • WebSocket delivery. For local development or services without a public URL, Posthook delivers via WebSocket. No tunnels required. Use the CLI for interactive local testing.

Compared to building it yourself

CapabilityDIY (cron + reminders table)Posthook
Per-user schedulingInsert a row, poll for due rows on a cron cadenceOne API call. Fires at the scheduled time.
Retry on failureBuild retry logic, track attempts, handle backoffRetries with backoff, jitter, and per-hook overrides
Delivery visibilityQuery your own logs or build a dashboardPer-hook status with attempt history
Failure alertingBuild monitoring around the cron jobPer-endpoint anomaly detection with alerts via email, Slack, or webhook
Bulk recovery after an incidentWrite a SQL script per incidentBulk retry by endpoint and time range
Timezone and DSTConvert to UTC, handle DST in codepostAtLocal + timezone, DST handled at scheduling time
CancellationUpdate a flag, handler checks before actingposthook.hooks.delete(hookId) for cleanup
Application stateYou manage itYou manage it
InfrastructureCron job, database table, polling query, retry logic, monitoringAPI key, endpoint

Why a reminders table tends to grow into a subsystem

A reminders table polled by cron is the usual starting point. It works longer than most teams expect, which is why the maintenance burden often arrives late: per-user precision, polling drift, failure visibility, and the compounding burden of each new reminder type. By the fifth concern, you’re maintaining a small subsystem you didn’t set out to build. The reminders and follow-ups pattern walks through the design decisions that make a reliable reminder system work.

When not to use Posthook for reminders

You need a template editor and engagement analytics. If marketing or product people edit notification templates without code deploys and you need open rates, click tracking, and A/B testing, use a customer engagement platform like Customer.io or Braze. Posthook does not send notifications. It triggers your application to send them.

Your reminders are simple batch digests. “Send a daily summary email to all users at 8am” is a cron query for eligible users. Posthook fits when each user, event, or entity creates its own scheduled action with its own deadline.

You need complex journey orchestration. If the logic involves branching (“if the user opened email A, send variant B; otherwise send variant C”), that’s workflow orchestration. Consider Inngest or Trigger.dev or Temporal. Posthook handles the scheduling, not the branching logic.

Pricing for reminder workloads

Posthook charges per scheduled hook. Retries, deliveries, and API calls don’t count toward the quota.

  • Free: 1,000 hooks/month. Suits early-stage testing and small workloads.
  • Launch: $39/month for 20,000 hooks. May fit teams sending up to roughly 20K reminders/month.
  • Growth: $99/month for 100,000 hooks.
  • Scale: $249/month for 500,000 hooks.

A trial flow with three reminders per signup is three hooks per signup. An appointment flow with two reminders per booking is two hooks per booking. See pricing for the full breakdown.

Getting started

Install the SDK:

npm install @posthook/node

Schedule a reminder relative to now:

import Posthook from '@posthook/node';

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

await posthook.hooks.schedule({
  path: '/webhooks/onboarding-nudge',
  postIn: '48h',
  data: { userId: 'user_123' },
});

Or in the user’s local time, with DST handled at scheduling time:

await posthook.hooks.schedule({
  path: '/webhooks/morning-reminder',
  postAtLocal: '2026-05-01T09:00:00',
  timezone: 'America/New_York',
  data: { userId: 'user_123' },
});

Point the path at a handler on your service. When the hook fires, your handler checks current state and decides whether to send.

For per-hook retry overrides, cancellation, bulk replay, and the full API, see the Posthook docs.

Frequently asked questions

Ready to get started?

Free plan includes 1,000 hooks/month. No credit card required.