# A run started with an API key finished a turn

`POST` to your webhook URL (`runFinished`)

Sent to the `webhook_url` set on the API key that started the run (PATCH /o/{org}/api-keys/{key}), within about 30 seconds of a turn finishing: one per turn of a resumable run. Signed per Standard Webhooks with the key's `whsec_` secret. Any 2xx within 5 seconds acknowledges it; anything else is retried 2, 4 and 6 minutes after the first attempt with the same `webhook-id`, then dropped. Revoking the key or clearing its webhook drops pending retries.

## Headers

- `webhook-id`: Stable across retries: webhook:{run_id}:{completed_at ms}.
- `webhook-timestamp`: Unix seconds of this attempt.
- `webhook-signature`: v1,<base64 HMAC-SHA256 of "{webhook-id}.{webhook-timestamp}.{body}", keyed by the base64 bytes after whsec_>.

## Payload

- `type` (string, required, one of `run.completed`, `run.failed`, `run.cancelled`)
- `timestamp` (string, required): When the turn finished.
- `data` (object, required)
  - `run_id` (string, required)
  - `project_id` (string | null, required)
  - `status` (string, required): completed, needs_review, failed or cancelled.
  - `result` (object | null, required): The turn's answer, as GET …/sessions/{session}/result returns it; null when there is none or it is over 16 KiB (read result_url).
    - `text` (string | null, optional)
    - `structured` (any, optional): The object output_schema asked for, when one was given.
    - `schema_valid` (boolean | null, optional)
  - `total_cost_usd` (number | null, required)
  - `result_url` (string, required)
  - `events_url` (string, required)
  - `artifacts_url` (string, required)

## Verify the signature

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: the key's whsec_… webhook_secret. body: the raw request body, unparsed.
export function verify(secret: string, headers: Headers, body: string): boolean {
  const id = headers.get("webhook-id")!;
  const ts = headers.get("webhook-timestamp")!;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // stale or replayed
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest();
  return (headers.get("webhook-signature") ?? "").split(" ").some((sig) => {
    const got = Buffer.from(sig.split(",")[1] ?? "", "base64");
    return sig.startsWith("v1,") && got.length === expected.length && timingSafeEqual(got, expected);
  });
}
```

```python
import base64
import hashlib
import hmac
import time


# secret: the key's whsec_… webhook_secret. body: the raw request body, unparsed.
def verify(secret: str, headers: dict, body: bytes) -> bool:
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False  # stale or replayed
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{ts}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(
        sig.startswith("v1,") and hmac.compare_digest(sig[3:], expected)
        for sig in headers.get("webhook-signature", "").split(" ")
    )
```

```http
POST /your/webhook/url HTTP/1.1
Content-Type: application/json
webhook-id: webhook:ce_01J9ZK7Q:1791288000000
webhook-timestamp: 1791288000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{
  "type": "run.completed",
  "timestamp": "2026-10-06T15:09:03.000Z",
  "data": {
    "run_id": "3f9c2a71d04b8e65",
    "project_id": null,
    "status": "completed",
    "result": {
      "text": "Pinned the clock in checkout.spec.ts and opened PR 412.",
      "structured": null,
      "schema_valid": null
    },
    "total_cost_usd": 1.84,
    "result_url": "https://api.prix.dev/o/acme/p/_/sessions/3f9c2a71d04b8e65/result",
    "events_url": "https://api.prix.dev/o/acme/p/_/sessions/3f9c2a71d04b8e65/events",
    "artifacts_url": "https://api.prix.dev/o/acme/p/_/sessions/3f9c2a71d04b8e65/artifacts"
  }
}
```

## Responses

### 200

Any 2xx acknowledges the delivery.

## Example response (200)

```http
HTTP 200 Any 2xx acknowledges the delivery.
```
