Rundunrundun

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:

  1. When an event fires, Rundun's API enqueues a delivery job
  2. A Worker consumer picks up the job and delivers it to your webhook URL
  3. If delivery fails (non-2xx or timeout), the Worker re-enqueues with an increasing delay
  4. 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

  1. Set up an HTTPS endpoint that accepts POST requests and returns 2xx quickly

  2. 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
  }'
  1. Verify the signature on each incoming request — see Signatures

  2. Use event_id as 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.