Skip to main content
Posthook powers user engagement workflows that require timing delays, such as reminders or abandoned cart recovery.

Event Reminders

Send a reminder the evening before a scheduled event, at 5 PM in the user’s local timezone.
  1. Schedule: When the user books, schedule the reminder for the day before at 5 PM local.
  2. Handle: When the hook fires, send the email.
import Posthook from '@posthook/node';
import { Signatures, SignatureVerificationError } from '@posthook/node';

const posthook = new Posthook(process.env.POSTHOOK_API_KEY);
const signatures = new Signatures(process.env.POSTHOOK_SIGNING_KEY);

/* 1. Schedule */
async function onWebinarBooking(booking) {
  const reminder = new Date(booking.date);
  reminder.setUTCDate(reminder.getUTCDate() - 1);
  reminder.setUTCHours(17, 0, 0, 0);

  await posthook.hooks.schedule({
    path: '/webhooks/send-email',
    postAtLocal: reminder.toISOString().split('.')[0],
    timezone: booking.timezone,
    // LEAST PRIVILEGE: Only send the ID. Don't send PII or template logic.
    data: {
      bookingId: booking.id,
    }
  });
}

/* 2. Handle */
app.post('/webhooks/send-email', async (req, res) => {
  try {
    const delivery = signatures.parseDelivery(req.body, req.headers);

    const { bookingId } = delivery.data;
    const booking = await db.getBooking(bookingId);

    // VALIDITY CHECKS:
    if (!booking || booking.status === 'cancelled') {
      return res.status(200).send('Booking cancelled or not found');
    }

    if (booking.reminderSent) {
      return res.status(200).send('Reminder already sent');
    }

    // Derive logic from your DB, not the payload
    const template = booking.type === 'webinar' ? 'webinar_reminder' : 'event_reminder';

    await emailService.send(booking.userId, template, { event: booking.event });
    await db.markReminderSent(bookingId);

    res.status(200).send('Reminder sent');
  } catch (err) {
    if (err instanceof SignatureVerificationError) {
      return res.status(401).json({ error: err.message });
    }
    throw err;
  }
});
The reminderSent check prevents most duplicates, but there’s a small race window if a retry arrives before the flag is committed. If you need stricter guarantees, see Idempotency.

Abandoned Cart Recovery

If a user adds items to their cart but doesn’t checkout, schedule a “nudge” email for 30 minutes later. Instead of trying to cancel the hook when a user purchases, it’s often simpler to verify validity at execution time.
  1. Schedule: When the user adds an item, schedule the email for 30 minutes later.
  2. Verify: When the webhook handler receives the request, check if the cart has been converted to an order.
import Posthook from '@posthook/node';
import { Signatures, SignatureVerificationError } from '@posthook/node';

const posthook = new Posthook(process.env.POSTHOOK_API_KEY);
const signatures = new Signatures(process.env.POSTHOOK_SIGNING_KEY);

/* 1. Schedule the Nudge */
async function onAddToCart(cart) {
  await posthook.hooks.schedule({
    path: '/webhooks/cart/abandoned',
    postIn: '30m',
    data: {
      cartId: cart.id
    }
  });
}

/* 2. Handle & Verify */
app.post('/webhooks/cart/abandoned', async (req, res) => {
  try {
    const delivery = signatures.parseDelivery(req.body, req.headers);

    const { cartId } = delivery.data;
    const cart = await db.getCarts(cartId);

    // IDEMPOTENCY CHECK:
    // If the cart is already "completed" or "paid", do nothing.
    if (cart.status === 'completed') {
      return res.status(200).send('Cart already recovered');
    }

    // Otherwise, send the email
    await sendRecoveryEmail(cart.userId);
    res.status(200).send('Email sent');
  } catch (err) {
    if (err instanceof SignatureVerificationError) {
      return res.status(401).json({ error: err.message });
    }
    throw err;
  }
});