> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sente.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Subscribe to inbound mail and run events pushed to your endpoint.

Webhooks push events to your HTTPS endpoint so your agent reacts to inbound mail and run outcomes without polling. Guide: [Webhooks](/guides/webhooks).

## Events

| Event              | Fires when                                                                             |
| ------------------ | -------------------------------------------------------------------------------------- |
| `message.received` | An inbound email lands in an identity's inbox (after annotation). Queued with retries. |
| `run.blocked`      | A run pauses for a human. Single delivery attempt.                                     |
| `run.completed`    | A run finishes successfully. Single delivery attempt.                                  |
| `run.failed`       | A run fails. Single delivery attempt.                                                  |

<Note>
  The `run.*` events are fired once, without retry — pair them with polling `GET /v1/runs/:id` if your endpoint can miss deliveries. `message.received` is queued and retried.
</Note>

## Delivery

Each delivery is an HTTP `POST` with `content-type: application/json` and a 10-second timeout. Every delivery to your org carries the **org-level secret** in the `x-sente-secret` header — verify it with a constant-time compare before trusting the payload:

```
POST /your/endpoint HTTP/1.1
content-type: application/json
x-sente-secret: whsec_k3v9...
```

Webhook URLs must be `http(s)` and resolve to public addresses. URLs pointing at loopback, private-range, link-local, or cloud-metadata addresses are rejected at registration (`400`) and re-checked before every delivery.

## Create a webhook

`POST /v1/webhooks`

| Field        | Type   | Required | Description                                                                      |
| ------------ | ------ | -------- | -------------------------------------------------------------------------------- |
| `url`        | URL    | yes      | Your HTTPS endpoint. Must resolve to a public address.                           |
| `events`     | array  | yes      | One or more of `message.received`, `run.completed`, `run.failed`, `run.blocked`. |
| `identityId` | string | no       | Scope to one identity. Omitted → fires for all the org's identities.             |

Idempotent per (url, identity scope): re-posting the same url and scope returns the existing subscription with its events replaced — safe to call on every app start.

```bash theme={null}
curl -X POST https://api.sente.run/v1/webhooks \
  -H "Authorization: Bearer sk_sente_..." \
  -H "content-type: application/json" \
  -d '{
    "url": "https://agent.example.com/sente/events",
    "events": ["message.received", "run.blocked", "run.completed", "run.failed"]
  }'
```

Response `201` (the only place the secret is returned by this API — store it):

```json theme={null}
{
  "id": "whk_4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d",
  "url": "https://agent.example.com/sente/events",
  "events": ["message.received", "run.blocked", "run.completed", "run.failed"],
  "identityId": null,
  "secret": "whsec_k3v9m2q8..."
}
```

Errors: `400` with zod issues for a bad `events` value; `400 {"error": "webhook url not allowed: ..."}` for a non-public URL; `404 {"error": "identity not found"}` for a foreign `identityId`.

## List webhooks

`GET /v1/webhooks`

```bash theme={null}
curl https://api.sente.run/v1/webhooks \
  -H "Authorization: Bearer sk_sente_..."
```

Response `200` (no secret in list responses):

```json theme={null}
[
  {
    "id": "whk_4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d",
    "url": "https://agent.example.com/sente/events",
    "events": ["message.received", "run.blocked", "run.completed", "run.failed"],
    "identityId": null,
    "createdAt": "2026-07-20T09:00:00.000Z"
  }
]
```

## Delete a webhook

`DELETE /v1/webhooks/:id`

```bash theme={null}
curl -X DELETE https://api.sente.run/v1/webhooks/whk_4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d \
  -H "Authorization: Bearer sk_sente_..."
```

Response: `204` with no body. `404 {"error": "not found"}` otherwise.

## Payloads

### message.received

```json theme={null}
{
  "type": "message.received",
  "message": {
    "id": "msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b",
    "channel": "email",
    "identity": { "id": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5", "name": "support-agent", "email": "acme-qa@sente.run" },
    "from": "no-reply@example-app.com",
    "to": "acme-qa@sente.run",
    "subject": "Your verification code",
    "createdAt": "2026-07-20T09:15:02.114Z"
  }
}
```

The payload is a summary — fetch the body and annotation with [`GET /v1/messages/:id`](/api-reference/messages).

### run.blocked

Fired the moment a run parks for a human. Carries everything a notifier needs to route someone to the gate:

```json theme={null}
{
  "type": "run.blocked",
  "run": {
    "id": "run_8d9e0f1a2b3c4d5e6f708192a3b4c5d6",
    "type": "register",
    "status": "blocked",
    "code": "CAPTCHA_REQUIRED",
    "detail": "captcha on the signup form",
    "confirmBeforeSubmit": false,
    "registrationId": "reg_1c2d3e4f5a6b708192a3b4c5d6e7f801",
    "appUrl": "https://app.example.com/signup",
    "identity": { "id": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5", "name": "support-agent", "email": "acme-qa@sente.run" },
    "liveViewUrl": "https://app.hyperbrowser.ai/live?...",
    "actionUrl": "https://app.sente.run/runs/run_8d9e0f1a2b3c4d5e6f708192a3b4c5d6",
    "holdExpiresAt": "2026-07-20T11:15:00.000Z",
    "createdAt": "2026-07-20T11:00:00.000Z"
  }
}
```

`actionUrl` deep-links to the dashboard run page (live view + resume in one place). `holdExpiresAt` is when the run auto-fails `BLOCKED_TIMEOUT` if nobody acts — your notification can say how long the human has. The `code` values are listed on [Runs](/api-reference/runs).

### run.completed / run.failed

```json theme={null}
{
  "type": "run.completed",
  "run": {
    "id": "run_8d9e0f1a2b3c4d5e6f708192a3b4c5d6",
    "type": "register",
    "status": "completed",
    "code": null,
    "detail": null,
    "registrationId": "reg_1c2d3e4f5a6b708192a3b4c5d6e7f801",
    "appUrl": "https://app.example.com/signup",
    "identity": { "id": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5", "name": "support-agent", "email": "acme-qa@sente.run" },
    "result": { "answer": "STATE: LOGGED_IN", "username": "acme-qa@sente.run" },
    "createdAt": "2026-07-20T11:00:00.000Z",
    "endedAt": "2026-07-20T11:03:12.500Z"
  }
}
```

`run.failed` has the same shape with `"status": "failed"`, the failure `code`/`detail` set, and `"result": null`.
