Webhook events
All events share the same outer envelope. The data object is event-specific.
Common envelope
{
"event_id": "evt-550e8400-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "step.completed",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-22T08:33:12Z",
"identity": {
"mode": "member",
"user_id": "u-abc12300-e29b-41d4-a716-446655440000"
},
"data": { }
}
| Field | Description |
|---|---|
event_id |
Stable UUID per event — identical across all retry attempts. Use as idempotency key. |
delivery_attempt |
Which attempt this is (1–5). |
event |
One of the five event type strings. |
run_id |
The run this event belongs to. |
template_id |
The template this run is based on. |
template_v |
Template version snapshotted at run creation. |
ts |
ISO 8601 timestamp of when the event fired. |
identity |
How the executor identified themselves — anonymous, external, or member. |
data |
Event-specific payload — see each event below. |
step.completed
Fires after each step is answered (all required photos for that step have been captured).
Photos are embedded as base64 JPEG thumbnails — approximately 25KB each, so a step with 5 photos adds about 125KB to the payload. This keeps the event self-contained for live progress tracking.
{
"event_id": "evt-550e8400-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "step.completed",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-22T08:33:12Z",
"identity": { "mode": "member", "user_id": "u-abc12300-e29b-41d4-a716-446655440000" },
"data": {
"step_id": "s-dm01",
"step_label": "Any pre-existing damage?",
"step_type": "boolean",
"answer": true,
"photos": [],
"geo": null,
"progress": { "total": 6, "answered": 3, "skipped": 0 }
}
}
With photos (step has photo overlay)
{
"event_id": "evt-7a3f2100-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "step.completed",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-22T08:35:00Z",
"identity": { "mode": "member", "user_id": "u-abc12300-e29b-41d4-a716-446655440000" },
"data": {
"step_id": "s-dm02",
"step_label": "Damage type",
"step_type": "multi_choice",
"answer": ["o-dm1", "o-dm3"],
"photos": [
{
"photo_index": 0,
"preview": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
"width": 320,
"height": 240
}
],
"geo": { "lat": 51.507, "lng": -0.127, "accuracy_m": 5 },
"progress": { "total": 6, "answered": 4, "skipped": 0 }
}
}
Answer shapes by step type
| Step type | answer value |
|---|---|
boolean |
true or false |
choice |
"o-fu2" (option ID) |
multi_choice |
["o-dm1", "o-dm3"] (array of option IDs) |
text |
"Scratch on rear left door" |
number |
42150 |
rating |
4 |
photo |
null (the answer is the photos array) |
signature |
"https://media.rundun.app/runs/r-.../s-sg01/sig.png" |
barcode |
"VIN1234567890" |
datetime |
"2026-05-10" or "2026-05-10T14:00:00Z" |
instruction |
null (acknowledged) |
step.skipped
Fires when a non-required step is explicitly skipped by the executor. Only fires if settings.allow_skip is true on the template.
{
"event_id": "evt-7f3a1200-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "step.skipped",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-22T08:34:01Z",
"identity": { "mode": "member", "user_id": "u-abc12300-e29b-41d4-a716-446655440000" },
"data": {
"step_id": "s-nt01",
"step_label": "Additional notes",
"step_type": "text",
"progress": { "total": 6, "answered": 3, "skipped": 1 }
}
}
run.completed
Fires when all required steps are answered and the executor submits the run.
Photos in run.completed are full-resolution URLs — not base64 thumbnails. The background upload from the device to Rundun's cloud storage completes between step answers, so by the time the run is submitted the URLs are available.
{
"event_id": "evt-9c2b0400-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "run.completed",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-22T08:41:05Z",
"identity": { "mode": "member", "user_id": "u-abc12300-e29b-41d4-a716-446655440000" },
"data": {
"duration_seconds": 612,
"progress": { "total": 6, "answered": 5, "skipped": 1 },
"answers": {
"s-od01": {
"answer": 42150,
"photos": [],
"geo": null
},
"s-fu01": {
"answer": "o-fu2",
"photos": [],
"geo": null
},
"s-dm01": {
"answer": true,
"photos": [],
"geo": null
},
"s-dm02": {
"answer": ["o-dm1", "o-dm3"],
"photos": [
{
"photo_index": 0,
"url": "https://media.rundun.app/runs/r-550e8400-e29b-41d4-a716-446655440000/s-dm02/photo_0.jpg"
}
],
"geo": { "lat": 51.507, "lng": -0.127, "accuracy_m": 5 }
},
"s-dm03": {
"answer": "Scratch on rear left door, crack on front bumper",
"photos": [],
"geo": null
},
"s-sg01": {
"answer": "https://media.rundun.app/runs/r-550e8400-e29b-41d4-a716-446655440000/s-sg01/sig.png",
"photos": [],
"geo": null
}
}
}
}
data field |
Description |
|---|---|
duration_seconds |
Seconds from run creation to submission |
progress |
Final step counts |
answers |
Full answer map keyed by step ID — same answer shapes as step.completed |
run.cancelled
Fires when the executor explicitly exits the run before completing it.
{
"event_id": "evt-4e8d0100-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "run.cancelled",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-22T08:37:22Z",
"identity": { "mode": "member", "user_id": "u-abc12300-e29b-41d4-a716-446655440000" },
"data": {
"reason": "user_exit",
"progress": { "total": 6, "answered": 3, "skipped": 0 },
"last_step_id": "s-dm02",
"answers_so_far": {
"s-od01": { "answer": 42150, "photos": [], "geo": null },
"s-fu01": { "answer": "o-fu2", "photos": [], "geo": null },
"s-dm01": { "answer": true, "photos": [], "geo": null }
}
}
}
data field |
Description |
|---|---|
reason |
Always "user_exit" — executor tapped the exit/cancel control |
progress |
Step counts at the point of cancellation |
last_step_id |
The step that was active when the executor cancelled |
answers_so_far |
Partial answers collected before cancellation |
run.expired
Fires when the run link expires before the executor completes it. If expires_in_hours was not set on the run, this event never fires.
Expiry is checked on access — the event fires when the executor (or the system) attempts to open an expired run, not at the exact expiry time.
{
"event_id": "evt-2b5f0900-e29b-41d4-a716-446655440000",
"delivery_attempt": 1,
"event": "run.expired",
"run_id": "r-550e8400-e29b-41d4-a716-446655440000",
"template_id": "t-a3f9bc00-e29b-41d4-a716-446655440000",
"template_v": 3,
"ts": "2026-05-23T08:31:00Z",
"identity": { "mode": "anonymous" },
"data": {
"expired_at": "2026-05-23T08:31:00Z",
"progress": { "total": 6, "answered": 0, "skipped": 0 }
}
}
The identity is anonymous when the run expired without being opened. If the executor opened the run and partially completed it before expiry, identity reflects how they identified.