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

# Webhooks

> Signed POSTs when a machine is ready, pauses, stops, fails or is deleted.

Register an HTTPS endpoint for an org, and InstaBox POSTs a signed JSON event to it when one of the
org's machines or snapshots changes state. You do not need to poll `GET /v1/machines/{id}`.

```bash CLI theme={null}
instabox webhooks create https://example.com/hook --event sandbox.started --event sandbox.error
instabox webhooks ls
instabox webhooks test wh_...
instabox webhooks deliveries wh_...
```

```bash curl theme={null}
curl -X POST https://api.instabox.sh/v1/orgs/$ORG/webhooks -H "Authorization: Bearer $INSTABOX_API_KEY" \
  -H 'Content-Type: application/json' -d '{"url":"https://example.com/hook","events":["sandbox.started","sandbox.error"]}'
```

* Without `--event`, the endpoint subscribes to every type. The types are under [Reference](#reference).
* `create` and `rotate` print the signing secret (`whsec_…`) once.
* The URL must be public `https`. Redirects are not followed.
* The dashboard's **Webhooks** page does the same, with a delivery log.

> [accordion] Who can manage endpoints, and more CLI
>
> The org's owner or admin can, signed in or with an API key of that org. `machines:read` lists and reads
> deliveries, `machines:write` creates, updates, rotates, tests, and retries, and `machines:delete`
> deletes. With `INSTABOX_API_KEY` set, `--org` defaults to the key's org. `GET /v1/orgs` lists your
> orgs, and an API key sees only its own.
>
> Every `webhooks` subcommand takes `--json` or `-o json`, including `events`, `rm`, `test`, and
> `retry`. The routes are under `/v1/orgs/{id}/webhooks` in the
> [API reference](https://instabox.sh/docs/api-reference/introduction.md).

## Verify the signature

Requests follow [Standard Webhooks](https://www.standardwebhooks.com/), so its libraries verify them.

| Header | Value |
| - | - |
| `webhook-id` | the event ID, the same on every retry. Dedupe on it. |
| `webhook-timestamp` | Unix seconds. Reject timestamps more than 5 minutes off. |
| `webhook-signature` | `v1,` and the base64 HMAC-SHA256 of `<webhook-id>.<webhook-timestamp>.<raw body>`, keyed with the base64-decoded part of the secret after `whsec_` |

```js theme={null}
import crypto from 'node:crypto';
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const want = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
const ok = header.split(' ').some((s) => {
  const got = Buffer.from(s.startsWith('v1,') ? s.slice(3) : '', 'base64');
  return got.length === want.length && crypto.timingSafeEqual(got, want); // constant time
});
```

> [!NOTE]
>
> After a rotation, the header lists two signatures for 24 hours. Accept either, and compare in constant
> time.

## Answer each delivery in time

Answer `2xx` within 10 seconds of connecting: reply first, then do the work.

* **Order:** one machine's events arrive in order, also across retries. A later event waits while an
  earlier one is retried. Events of different machines interleave, so order those by `createdAt`.
* **Retries:** any other answer, and a timeout, is retried after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h,
  and 10 h. Then the delivery fails. A delivery still pending 3 days after its event fails as expired.
  `instabox webhooks retry` sends a finished one again.
* **Log:** deliveries are kept 30 days.

> [!WARNING]
>
> An endpoint that fails every attempt for 5 days is disabled, and the workspace's owner and admins get an
> email. To turn it back on, run `instabox webhooks update ID --enable`.

## Reference

### Events

| Type | When |
| - | - |
| `sandbox.created` | the machine is accepted (state `creating`) |
| `sandbox.started` | it is `running` and ready for exec and SSH (after create, start, resume, or wake) |
| `sandbox.stopped` | it is `stopped` |
| `sandbox.suspended` | it is paused (`suspended`): by you, when idle, or by a plan limit |
| `sandbox.error` | it is in `error` |
| `sandbox.deleted` | it is `deleted` |
| `snapshot.created`, `snapshot.failed`, `snapshot.deleted` | a snapshot is ready, failed to upload, or is deleted |
| `webhook.test` | only from a test send |

### Event body

```json theme={null}
{
  "id": "evt_…",
  "type": "sandbox.suspended",
  "createdAt": "…",
  "orgId": "org_…",
  "data": {
    "sandbox": { "id": "m_…", "name": "worker-1", "metadata": {} },
    "previousState": "suspending",
    "state": "suspended",
    "reason": "idle",
    "operationId": "op_…"
  }
}
```

`reason` says what moved the machine. Its values include `user`, `idle`, `archive`, `wake`, and
`system` (observed by the server, such as a crash).

> [card] [Lifecycle](https://instabox.sh/docs/machines/lifecycle.md)
>
> The states these events report.

> [card] [Auto-pause and max-run](https://instabox.sh/docs/machines/auto-pause.md)
>
> Why a machine pauses when idle or at a plan limit.
