Documentation Index
Fetch the complete documentation index at: https://docs.posthook.io/llms.txt
Use this file to discover all available pages before exploring further.
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.
- Schedule: When the user books, schedule the reminder for the day before at 5 PM local.
- 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.
- Schedule: When the user adds an item, schedule the email for 30 minutes later.
- 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;
}
});