Posthook with Express
Schedule durable timers from Express route handlers. No cron library, no Redis, no polling loop — just an API call and an endpoint.
In-memory timers — setTimeout, node-schedule, node-cron — do not survive deploys or restarts. BullMQ solves this with Redis and workers, but adds infrastructure for what is fundamentally a timing problem.
Posthook provides durable scheduling without the operational weight. Schedule a hook from any Express route handler, and receive the delivery at an Express endpoint when the time arrives. No Redis, no workers, no polling loop.
Install
npm install @posthook/node
Create the client:
import Posthook from '@posthook/node';
const posthook = new Posthook(process.env.POSTHOOK_API_KEY, {
signingKey: process.env.POSTHOOK_SIGNING_KEY,
});
Schedule a hook
When an order is placed, schedule a payment timeout check for 30 minutes later:
app.post('/orders', async (req, res) => {
const order = await db.createOrder(req.body);
await posthook.hooks.schedule({
path: '/webhooks/order-timeout',
postIn: '30m',
data: { orderId: order.id },
});
res.json({ orderId: order.id });
});
The hook is persisted by Posthook. It survives deploys, restarts, and scaling events.
Receive the delivery
The handler uses route-level express.raw() for signature verification, checks state, and decides whether to act:
app.post('/webhooks/order-timeout', express.raw({ type: '*/*' }), async (req, res) => {
let delivery;
try {
delivery = posthook.signatures.parseDelivery(req.body, req.headers);
} catch {
return res.sendStatus(401);
}
const order = await db.getOrder(delivery.data.orderId);
if (order.status !== 'pending') {
// Already paid, cancelled, or expired — safe to skip
return res.json({ status: 'skipped' });
}
await db.expireOrder(order.id);
await notifyCustomer(order, 'payment_timeout');
res.json({ status: 'expired' });
});
The express.raw({ type: '*/*' }) middleware is applied to this route only, so req.body is a raw Buffer for signature verification. Your other routes can use express.json() normally.
The state check makes this handler idempotent. Posthook guarantees at-least-once delivery — the handler may receive the same delivery more than once. Since it checks order.status before acting, duplicates resolve as no-ops.
Retry override for time-sensitive work
Payment timeouts are more critical than marketing reminders. Use retryOverride to get more aggressive retries without changing project defaults:
await posthook.hooks.schedule({
path: '/webhooks/order-timeout',
postIn: '30m',
data: { orderId: order.id },
retryOverride: {
minRetries: 5,
delaySecs: 10,
strategy: 'exponential',
backoffFactor: 2.0,
maxDelaySecs: 300,
},
});
This hook retries up to 5 times with exponential backoff — 10s, 20s, 40s, 80s, 160s — while other hooks in the project use the default strategy. Per-hook retry overrides are set at scheduling time.
Local development
Use the Posthook CLI to forward hook deliveries to your local Express server — no deploy, no public URL:
npx posthook listen --forward http://localhost:3000
The CLI connects via WebSocket and forwards each delivery as an HTTP POST to your local server with the same headers and payload as production. It automatically accepts or fails hooks based on your handler’s response status (2xx = accept, anything else = fail and retry). Schedule hooks normally from your app — they arrive at your local endpoint.
Next steps
- Quickstart guide — full setup walkthrough
- Expiration and timeout checks — the complete pattern with DIY comparison
- Reminders and follow-ups — onboarding, trials, follow-up chains
- BullMQ vs Posthook — when to use a queue and when to use managed scheduling
- Pricing — free plan includes 1,000 hooks/month
Frequently asked questions
Related patterns
Ready to get started?
Create your free account and start scheduling hooks in minutes. No credit card required.