# API reference — Webhooks

Events delivered to your endpoint as signed POST requests: `run.completed`, `episode.failed`, `threshold.crossed`, `regression.detected`, `estop.triggered`. Acknowledge with any 2xx.

## Event: run.completed

A run finished and its result is available

Sent when any run reaches `completed`. The payload carries the full result — n, rate, CI, clusters, both meters, cost.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | body | `string` | yes |  |
| type | body | `run.completed \| episode.failed \| threshold.crossed \| regression.detected \| estop.triggered` | yes |  |
| created | body | `string` | yes |  |
| data | body | `object` | yes |  |

Example payload:

```json
{
 "id": "evt_3301",
 "type": "run.completed",
 "created": "2026-08-29T11:37:48Z",
 "data": {
  "run_id": "run_8842",
  "result": {
   "n": 612,
   "success_rate": 0.874,
   "ci95": [
    0.846,
    0.898
   ],
   "cost_usd": 3812
  }
 }
}
```

## Event: episode.failed

An episode failed, with its cluster

Sent per failed episode when subscribed. `cluster` names the failure mode, e.g. `grasp_slip`.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | body | `string` | yes |  |
| type | body | `run.completed \| episode.failed \| threshold.crossed \| regression.detected \| estop.triggered` | yes |  |
| created | body | `string` | yes |  |
| data | body | `object` | yes |  |

Example payload:

```json
{
 "id": "evt_3312",
 "type": "episode.failed",
 "created": "2026-08-28T16:11:02Z",
 "data": {
  "run_id": "run_8842",
  "episode": 214,
  "cluster": "grasp_slip",
  "video_url": "https://artifacts.roborama.com/run_8842/ep_214/video.mp4"
 }
}
```

## Event: threshold.crossed

A threshold contract's target was met on soak tier

The contract escalates to the verification tier; `on_verified` fires after confirmation there.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | body | `string` | yes |  |
| type | body | `run.completed \| episode.failed \| threshold.crossed \| regression.detected \| estop.triggered` | yes |  |
| created | body | `string` | yes |  |
| data | body | `object` | yes |  |

Example payload:

```json
{
 "id": "evt_4407",
 "type": "threshold.crossed",
 "created": "2026-09-12T02:40:19Z",
 "data": {
  "contract_id": "run_9107",
  "checkpoint": "acme/skill-v4@ckpt-2210",
  "soak_result": {
   "n": 1188,
   "success_rate": 0.992,
   "ci95": [
    0.985,
    0.995
   ]
  },
  "escalating_to": "g1-edu-pro@fw2.3"
 }
}
```

## Event: regression.detected

A gate or matrix found a regression vs. a prior run

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | body | `string` | yes |  |
| type | body | `run.completed \| episode.failed \| threshold.crossed \| regression.detected \| estop.triggered` | yes |  |
| created | body | `string` | yes |  |
| data | body | `object` | yes |  |

Example payload:

```json
{
 "id": "evt_5150",
 "type": "regression.detected",
 "created": "2026-09-14T08:03:55Z",
 "data": {
  "run_id": "run_8901",
  "vs": "run_8821",
  "robot": "g1-edu-pro@fw2.4",
  "environment": "kitchen-std@v1.2",
  "delta_pts": -10.5,
  "cluster": "grasp_slip"
 }
}
```

## Event: estop.triggered

An emergency stop fired in a cell running your policy

The run is stopped, the episode is marked `estop`, and the cell is inspected before resuming. Partial results are kept.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | body | `string` | yes |  |
| type | body | `run.completed \| episode.failed \| threshold.crossed \| regression.detected \| estop.triggered` | yes |  |
| created | body | `string` | yes |  |
| data | body | `object` | yes |  |

Example payload:

```json
{
 "id": "evt_6001",
 "type": "estop.triggered",
 "created": "2026-09-02T19:22:31Z",
 "data": {
  "run_id": "run_8905",
  "cell": "cell-g1-04",
  "episode": 88,
  "reason": "workspace_intrusion"
 }
}
```
