Webhooks
Rundun delivers real-time events to your backend as runs progress. When an executor answers a step, submits a run, or cancels out, Rundun POSTs a signed JSON payload to your endpoint.
How it works
Webhook delivery in Rundun uses Rundun's delivery infrastructure:
- When an event fires, Rundun's API enqueues a delivery job
- A Worker consumer picks up the job and delivers it to your webhook URL
- If delivery fails (non-2xx or timeout), the Worker re-enqueues with an increasing delay
- After 5 attempts the event is marked
failed— no further retries
This architecture means delivery is decoupled from the executor's request path. The executor is never blocked waiting for your webhook to respond.
Subscription model
Webhook subscriptions are configured per-run at dispatch time. There is no global webhook setting. When you call POST /v1/runs, you include:
"webhook": {
"url": "https://your-app.com/hooks/rundun",
"secret": "whsec_abc123",
"events": ["run.completed", "run.cancelled", "run.expired"]
}
This means different runs can deliver to different endpoints, and the same template can be used with different webhook configurations for different integrations.
Events summary
| Event | Fires when | Payload highlights |
|---|---|---|
step.completed |
A step is answered (required photos taken) | Step ID, answer value, base64 photo thumbnails, geo, progress |
step.skipped |
A non-required step is explicitly skipped | Step ID, progress |
run.completed |
All required steps answered and run submitted | All answers, photo photo URLs, duration |
run.cancelled |
Executor exits before completion | Partial answers, last step ID, reason |
run.expired |
Run link expires before completion | Expiry time, zero-answered progress |
For full payload shapes with realistic examples, see Events.
Delivery SLA
- First attempt is immediate — within seconds of the event firing
- If the first attempt fails, retry schedule: 30s, 2 minutes, 10 minutes, 1 hour, 6 hours
- Total window before final failure: ~7.5 hours across 5 attempts
- Your endpoint must respond within 10 seconds per attempt
Quick start
-
Set up an HTTPS endpoint that accepts POST requests and returns 2xx quickly
-
Include webhook config in your run dispatch:
curl -X POST https://api.rundun.app/v1/runs \
-H "Authorization: Bearer rdk_live_..." \
-H "Content-Type: application/json" \
-d '{
"template_id": "t-550e8400-e29b-41d4-a716-446655440000",
"webhook": {
"url": "https://your-app.com/hooks/rundun",
"secret": "whsec_your_generated_secret",
"events": ["run.completed", "run.cancelled"]
},
"expires_in_hours": 48
}'
-
Verify the signature on each incoming request — see Signatures
-
Use
event_idas an idempotency key to handle duplicate deliveries safely
Idempotency
Each event has a stable event_id UUID — identical across all retry attempts for the same event. Your receiver should deduplicate on this field:
// Example idempotency check
const existing = await db.processedEvents.findOne({ event_id: payload.event_id })
if (existing) {
return res.status(200).json({ status: 'already_processed' })
}
// Process the event, then record it
await processEvent(payload)
await db.processedEvents.insertOne({ event_id: payload.event_id, processed_at: new Date() })
The delivery_attempt field (1–5) tells you which attempt this is — useful for logging and debugging, but not needed for idempotency.
Best practices
Respond immediately, process async. Your endpoint should return 2xx as fast as possible and enqueue the actual work for background processing. If your handler takes longer than 10 seconds, Rundun treats it as a failure and retries.
// Good: acknowledge fast, process async
app.post('/hooks/rundun', async (req, res) => {
res.status(200).json({ received: true }) // respond immediately
await queue.push(req.body) // process in background
})
Verify every request. Check the signature before trusting the payload. See Signatures.
Subscribe only to what you need. Omit step.completed if you only care about the final result — this reduces webhook volume significantly for long checklists.
Use GET /v1/runs/:run_id as a fallback. If a webhook delivery fails permanently, you can always pull the run state on demand via the API.