Handle webhook deliveries from Posthook in your application
When the scheduled time arrives, Posthook sends an HTTP POST request to your endpoint. This page covers the request format, how to handle deliveries, and production best practices.The destination URL combines your Project Domain (configured in settings) with the Path specified when scheduling the hook.
api.myapp.com (Domain) + /webhooks/remind (Path) = https://api.myapp.com/webhooks/remind (HTTPS is enforced)
Complete webhook handlers showing how to receive a delivery, verify the signature, and return a response. Each example uses the official SDK for signature verification.
Posthook guarantees at-least-once delivery. If your endpoint returns a non-2xx status code or times out (10-second limit), Posthook retries the delivery based on your project’s retry configuration.You can configure:
Choose fixed for constant delay or exponential for increasing delays with backoffFactor, maxDelaySecs, and jitter. See Scheduling: Per-hook retry override for the full field reference.
Because at-least-once delivery can produce rare duplicates under failure conditions, your hook handler should be idempotent. The right approach varies by use case. Some operations are naturally idempotent (e.g., setting a value in a database). Others need explicit deduplication.For cases that need it, include a unique ID in your payload (e.g., idempotency_key, event_id, or your own business logic ID like reminder_id). You can also use the id field from the hook delivery itself as your idempotency key.
When your handler receives the request, use a transaction to atomically claim the ID and do the work. If the work fails, the transaction rolls back and the retry can try again.
// IDEMPOTENCY: claim the ID and do the work in one transactionapp.post('/webhooks/posthook/remind', async (req, res) => { const { reminder_id, user_id } = req.body.data; const client = await pool.connect(); try { await client.query('BEGIN'); await client.query( 'INSERT INTO processed_hooks (idempotency_key) VALUES ($1)', [reminder_id] ); await sendReminder(user_id, reminder_id); await client.query('COMMIT'); } catch (err) { await client.query('ROLLBACK'); if (err.code === '23505') { // unique_violation: already processed return res.status(200).send('OK'); } throw err; } finally { client.release(); } res.status(200).send('OK');});
If the work fails (e.g., sendReminder errors), the transaction rolls back, the idempotency key is not recorded, and the hook is retried.
If you don’t use a relational database, you can achieve the same with Redis SET key NX EX ttl combined with a cleanup step on failure. The key is that the lock should only persist once the work succeeds.
Posthook enforces a 10-second timeout on webhook delivery. If your task takes longer than 10 seconds (e.g., generating a PDF, processing a video), your request will time out and be retried.You have two options:
Enable Async Hooks on your project and return HTTP 202. Posthook will wait for your ack/nack callback instead of timing out. This gives you built-in timeout management, retry on failure, and visibility into async processing status in the dashboard.