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.
Copy
// 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.To handle this, implement an Async Pattern:
Receive the webhook.
Push the job to an internal background queue (e.g., Redis, SQS, BullMQ).
Return 200 OK immediately to Posthook.
Process the job asynchronously in your worker.
If you need to track the completion of these long-running tasks (especially if they are 3rd-party APIs), check out the Recursive Polling pattern.