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

# Webhooks

> Get a signed HTTPS POST when a session finishes, work is filed, a report is ready or a scheduled run is skipped: endpoints, events, signatures and retries.

Webhooks push events to your own endpoint instead of making you poll. Each delivery is a signed `POST` with a JSON body.

## Managing endpoints

Manage endpoints in the dashboard, or from your own code with an **API key**:

| Door      | Routes                                     | Credential                                                                                                                                                          |
| --------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard | `/v2/org/webhooks`                         | Your signed-in session                                                                                                                                              |
| Your code | `https://webapp.attensira.com/v1/webhooks` | An `atn_live_` [API key](/account/api-keys). OAuth tokens are refused: creating an endpoint mints a signing secret, and an OAuth consent must not mint a credential |

Both doors take the same calls:

| Call                          | Does                                                                          |
| ----------------------------- | ----------------------------------------------------------------------------- |
| `GET /v1/webhooks`            | Lists the org's endpoints, secrets masked, each with its last delivery status |
| `POST /v1/webhooks`           | Creates an endpoint. Answers `201`                                            |
| `PATCH /v1/webhooks/{id}`     | Edits `url`, `events`, `description`, `project_id` or `enabled`               |
| `DELETE /v1/webhooks/{id}`    | Deletes an endpoint. Deliveries already queued for it are dropped             |
| `POST /v1/webhooks/{id}/test` | Sends a `webhook.test` delivery. Answers `202` with `{delivery_id, queued}`   |

An org can have at most **20** endpoints.

### Endpoint fields

| Field         | Notes                                         |
| ------------- | --------------------------------------------- |
| `url`         | Required. `https` only, on a public host      |
| `events`      | Which events to send. Empty or `*` means all  |
| `project_id`  | Optional. Only send events for this workspace |
| `description` | Optional. For your own reference              |
| `enabled`     | Whether deliveries are sent                   |

```bash Shell theme={null}
curl -X POST https://webapp.attensira.com/v1/webhooks \
  -H "Authorization: Bearer atn_live_<your key>" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/attensira","events":["session.finished","work_item.created"],"description":"Ops channel"}'
```

The response to the create call is the **only time the signing secret is shown** in full. Store it then; every later read shows it masked.

An endpoint also reports how deliveries are going: `last_delivery_at`, `last_status`, `last_error` and `failing_since`.

## Events

| Event               | Sent when                                                                                                     | `data`                                                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.finished`  | A session ends                                                                                                | `session_id`, `status` (`complete` or `failed`), `state` (`completed`, `failed` or `cancelled`), `origin`, `title`, `automation_id`, `created_at`, `ended_at` |
| `work_item.created` | The agent files a piece of work                                                                               | `work_item_id`, `title`, `kind`, `target`                                                                                                                     |
| `report.ready`      | A [client report](/agencies/client-reports) is published                                                      | `report_id`, `kind`, `title`, `url`                                                                                                                           |
| `loop.skipped`      | A scheduled automation run is skipped (see [loop health](/agent/loop-health#why-a-scheduled-run-was-skipped)) | `automation_id`, `name`, `reason`, `due_at`                                                                                                                   |
| `webhook.test`      | You asked for a test delivery                                                                                 | —                                                                                                                                                             |

## A delivery

Each delivery is a `POST` to your URL with a JSON body:

```json Body theme={null}
{
  "id": "evt_01J8Z3",
  "type": "session.finished",
  "created_at": "2026-09-23T06:04:11Z",
  "project_id": "prj_5b22",
  "data": {
    "session_id": "ses_9d02",
    "status": "complete",
    "state": "completed",
    "origin": "automation",
    "title": "Daily win plan",
    "automation_id": "aut_71cd",
    "created_at": "2026-09-23T06:00:02Z",
    "ended_at": "2026-09-23T06:04:10Z"
  }
}
```

With these headers:

| Header                | Value                                                   |
| --------------------- | ------------------------------------------------------- |
| `Attensira-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>`                 |
| `Attensira-Event`     | The event type                                          |
| `Attensira-Delivery`  | The delivery id. Stable across retries, so dedupe on it |

## Verifying the signature

`v1` is the hex HMAC-SHA256, keyed with the endpoint's signing secret, of the string `<t>.<body>`: the `t` value, a full stop, and the raw request body exactly as received.

1. Split the header on `,` and read `t` and `v1`.
2. Compute the HMAC over `t + "." + rawBody`.
3. Compare it with `v1` in constant time.
4. Reject the delivery if `t` is more than **5 minutes** old.

```js Node theme={null}
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

## Retries

Answer with any `2xx` to accept a delivery. Any other status, or no answer within **10 seconds**, is retried up to **14 times** with exponential backoff, spread over about a day. Redirects are not followed: a `3xx` counts as a failure.

Because a retry carries the same `Attensira-Delivery`, a receiver that stores the ids it has handled can treat a repeat as already done.
