# Webhooks

> Signed event deliveries (Standard Webhooks) for sessions, machines, runs and feedback, with retries and redelivery.

Webhooks push events to your HTTPS endpoint so you do not have to poll.

## Events

| Event | When |
|---|---|
| `session.ready` | A session became ready |
| `session.failed` | A session failed |
| `session.closed` | A session closed |
| `worker.offline` / `worker.online` | A machine stopped or resumed sending heartbeats |
| `run.completed` | An automation run finished |
| `feedback.updated` | One of your feedback tickets changed |

## Create an endpoint

```bash
curl -X POST https://browlix.com/api/public/v1/webhooks \
  -H "Authorization: Bearer $BROWLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"command_id\":\"$(uuidgen)\",\"url\":\"https://example.com/browlix\",\"events\":[\"session.ready\",\"session.closed\"]}"
```

The URL must be `https://` on a public host name, on port 443, 2053, 2083, 2087, 2096 or 8443. The reply contains the signing secret (`whsec_…`) **once**; store it in your secret manager.

## Verify every request

Each delivery carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers ([Standard Webhooks](https://www.standardwebhooks.com) v1). Verify the **raw** body before parsing it:

```ts tab=TypeScript
import { verifyWebhook, WebhookVerificationError } from "@browlix/sdk";

app.post("/browlix", express.text({ type: "application/json" }), async (req, res) => {
  try {
    const event = await verifyWebhook<{ id: string; type: string; data: unknown }>(
      process.env.BROWLIX_WEBHOOK_SECRET!, req.headers, req.body,
    );
    await handleOnce(event); // a repeated webhook-id is the same event redelivered
    res.sendStatus(204);
  } catch (e) {
    res.sendStatus(e instanceof WebhookVerificationError ? 400 : 500);
  }
});
```

```python tab=Python
from standardwebhooks.webhooks import Webhook  # pip install standardwebhooks

wh = Webhook(os.environ["BROWLIX_WEBHOOK_SECRET"])
event = wh.verify(raw_body, headers)  # raises on a bad signature or a stale timestamp
```

Timestamps more than 5 minutes old or ahead are rejected. Handle each `webhook-id` once: retries and redeliveries reuse it.

## Retries

A delivery that does not get a `2xx` within the time limit is retried up to 8 attempts in total, after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. `webhooks.deliveries` lists attempts with status and duration; `webhooks.redeliver` sends a finished delivery again with the same `webhook-id`; `webhooks.test` sends one signed `webhook.test` event.

## Rotating the secret

`webhooks.rotate_secret` returns a new secret once. For `overlap_hours` the old secret keeps signing too — each request carries both signatures — so your receiver can switch without missing events.
