Schedule Webhooks from Express
Schedule webhooks from Express route handlers. Retries, delivery tracking, and alerting are built in. No BullMQ, no Redis, no polling loop.
Last updated: March 30, 2026
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 handles the scheduling. One API call to schedule a hook from any Express route handler, and the delivery arrives at an Express endpoint when the time comes. 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:
import { SignatureVerificationError } from '@posthook/node';
app.post('/webhooks/order-timeout', express.raw({ type: '*/*' }), async (req, res) => {
let delivery;
try {
delivery = posthook.signatures.parseDelivery(req.body.toString(), req.headers);
} catch (err) {
if (err instanceof SignatureVerificationError) {
return res.status(401).json({ error: err.message });
}
return res.status(500).json({ error: 'Internal error' });
}
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. The .toString() converts it to the string that parseDelivery expects 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. Your handler’s response determines the outcome: 200 means delivered, 202 starts an async hook flow, and anything else triggers a retry. Retries work the same way as in production, with the same backoff and attempt limits.
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 a webhook scheduler
- Pricing — free plan includes 1,000 hooks/month
Frequently asked questions
Ready to get started?
Create your free account and start scheduling hooks in minutes. No credit card required.