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

# Webhooks

> Receive signed notifications when a Box becomes ready, errors, or finishes archiving.

Box webhooks send account-wide lifecycle events to your HTTPS endpoint. Use them to start work when a Box is usable, respond to provisioning failures, or continue an automation after a Box stops.

|                  | CLI                  | API                                   | SDKs                         | Dashboard                                                                 |
| ---------------- | -------------------- | ------------------------------------- | ---------------------------- | ------------------------------------------------------------------------- |
| Register         | `box webhook create` | `POST /webhooks`                      | `createWebhook`              | [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks)              |
| List / read      | `box webhook list`   | `GET /webhooks`, `GET /webhooks/{id}` | `listWebhooks`, `getWebhook` | [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks)              |
| Edit in place    | not available        | `PATCH /webhooks/{id}`                | `updateWebhook`              | not available                                                             |
| Rotate secret    | `box webhook rotate` | `POST /webhooks/{id}/rotate`          | `rotateWebhookSigningSecret` | [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks)              |
| Remove           | `box webhook remove` | `DELETE /webhooks/{id}`               | `deleteWebhook`              | [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks)              |
| Delivery history | not available        | not available                         | not available                | [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks) → Deliveries |

## Register an endpoint

<CodeGroup>
  ```bash CLI theme={null}
  box webhook create https://example.com/hooks/box \
    --name production \
    --event ready \
    --event error \
    --event archived
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/webhooks" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "production",
      "url": "https://example.com/hooks/box",
      "events": ["box.ready", "box.error", "box.archived"]
    }'
  ```

  ```ts TypeScript theme={null}
  const created = await box.createWebhook({
    webhookCreateRequest: {
      name: "production",
      url: "https://example.com/hooks/box",
      events: new Set(["box.ready", "box.error", "box.archived"]),
    },
  });
  console.log(created.webhook.id, created.secret);   // secret is shown once
  ```

  ```python Python theme={null}
  from ascii_box_sdk.models.webhook_create_request import WebhookCreateRequest

  created = box.create_webhook(WebhookCreateRequest(
      name="production",
      url="https://example.com/hooks/box",
      events=["box.ready", "box.error", "box.archived"],
  ))
  print(created.webhook.id, created.secret)   # secret is shown once
  ```
</CodeGroup>

The CLI's `--event` accepts `ready`, `error`, or `archived` with or without the `box.` prefix, and subscribes to all three when you omit it. The API and SDKs take the full `box.` form and **require** at least one event.

The call returns a signing secret beginning with `whsec_`. **It is shown only once.** Store it in your secret manager.

<Note>
  Endpoints must use HTTPS on port 443 and resolve only to public IP addresses. Redirects are not followed. An account can register up to 10 endpoint URLs.
</Note>

## Manage endpoints

<CodeGroup>
  ```bash CLI theme={null}
  box webhook list
  box webhook rotate <id>   # replaces the signing secret
  box webhook remove <id>
  ```

  ```bash curl theme={null}
  curl -sS "$BOX_API_BASE/webhooks" \
    -H "Authorization: Bearer $BOX_API_KEY"

  curl -sS "$BOX_API_BASE/webhooks/$WEBHOOK_ID" \
    -H "Authorization: Bearer $BOX_API_KEY"

  curl -sS -X PATCH "$BOX_API_BASE/webhooks/$WEBHOOK_ID" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"events":["box.error"]}'

  curl -sS -X POST "$BOX_API_BASE/webhooks/$WEBHOOK_ID/rotate" \
    -H "Authorization: Bearer $BOX_API_KEY"

  curl -sS -X DELETE "$BOX_API_BASE/webhooks/$WEBHOOK_ID" \
    -H "Authorization: Bearer $BOX_API_KEY"
  ```

  ```ts TypeScript theme={null}
  await box.listWebhooks();
  await box.getWebhook({ webhookId: "wh_0123456789abcdef01234567" });

  await box.updateWebhook({
    webhookId: "wh_0123456789abcdef01234567",
    webhookUpdateRequest: { events: new Set(["box.error"]) },
  });

  const rotated = await box.rotateWebhookSigningSecret({
    webhookId: "wh_0123456789abcdef01234567",
  });
  console.log(rotated.secret);

  await box.deleteWebhook({ webhookId: "wh_0123456789abcdef01234567" });
  ```

  ```python Python theme={null}
  from ascii_box_sdk.models.webhook_update_request import WebhookUpdateRequest

  box.list_webhooks()
  box.get_webhook("wh_0123456789abcdef01234567")

  box.update_webhook(
      "wh_0123456789abcdef01234567",
      WebhookUpdateRequest(events=["box.error"]),
  )

  rotated = box.rotate_webhook_signing_secret("wh_0123456789abcdef01234567")
  print(rotated.secret)

  box.delete_webhook("wh_0123456789abcdef01234567")
  ```
</CodeGroup>

The [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks) tab of the dashboard does the same, and is the only surface that shows delivery history.

<Note>
  Only the API and SDKs can change an existing endpoint's name, URL, or events in place (`PATCH /webhooks/{webhookId}`). The CLI and the dashboard have no edit: delete and re-create, which mints a new signing secret.
</Note>

During rotation, accept both the old and new secret briefly: an attempt already in flight can still carry the old signature. Deleting an endpoint removes queued deliveries, but an attempt already in flight can still arrive.

## Events

| Event          | Sent when                                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------------------- |
| `box.ready`    | A Box enters a usable state (`ready`, `idle`, or `running`) after being non-usable, including recovery from an error. |
| `box.error`    | A Box enters `error`.                                                                                                 |
| `box.archived` | A Box finishes stopping and enters `archived`.                                                                        |

Each state transition creates one immutable event per subscribed endpoint in the same database transaction as the state change.

## Payload

```json theme={null}
{
  "id": "evt_2af97cd17a8248bb8fe37653a235bc91",
  "type": "box.ready",
  "createdAt": "2026-08-11T12:00:00.000Z",
  "data": {
    "box": {
      "id": "bx_23456789",
      "name": "Production worker"
    },
    "previousState": "cloning",
    "state": "ready"
  }
}
```

The public Box ID is in `data.box.id`. Use the top-level event `id` as an idempotency key.

## Verify signatures

Every request includes:

| Header              | Value                                         |
| ------------------- | --------------------------------------------- |
| `X-Ascii-Event`     | Event type                                    |
| `X-Ascii-Delivery`  | Stable event ID                               |
| `X-Ascii-Timestamp` | Unix timestamp for this attempt               |
| `X-Ascii-Signature` | `v1=` followed by a hex HMAC-SHA256 signature |
| `X-Ascii-Attempt`   | Attempt number, starting at 1                 |

Compute the expected signature over the exact raw request body:

```text theme={null}
HMAC_SHA256(secret, delivery_id + "." + timestamp + "." + raw_body)
```

Compare signatures in constant time, reject stale timestamps, and deduplicate the delivery ID.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function verifyWebhook(rawBody: Buffer, headers: Headers, secret: string) {
    const id = headers.get("x-ascii-delivery") ?? "";
    const timestamp = headers.get("x-ascii-timestamp") ?? "";
    const supplied = (headers.get("x-ascii-signature") ?? "").replace(/^v1=/, "");
    const expected = createHmac("sha256", secret)
      .update(`${id}.${timestamp}.`)
      .update(rawBody)
      .digest("hex");

    const issuedAt = Number(timestamp);
    if (!Number.isFinite(issuedAt) || Math.abs(Date.now() / 1000 - issuedAt) > 300) return false;
    if (!/^[a-f0-9]{64}$/i.test(supplied)) return false;
    return timingSafeEqual(Buffer.from(supplied, "hex"), Buffer.from(expected, "hex"));
  }
  ```

  ```python Python theme={null}
  import hmac, re, time
  from hashlib import sha256

  def verify_webhook(raw_body: bytes, headers, secret: str) -> bool:
      delivery = headers.get("x-ascii-delivery", "")
      timestamp = headers.get("x-ascii-timestamp", "")
      supplied = headers.get("x-ascii-signature", "").removeprefix("v1=")

      try:
          if abs(time.time() - float(timestamp)) > 300:
              return False
      except ValueError:
          return False
      if not re.fullmatch(r"[a-fA-F0-9]{64}", supplied):
          return False

      expected = hmac.new(
          secret.encode(),
          f"{delivery}.{timestamp}.".encode() + raw_body,
          sha256,
      ).hexdigest()
      return hmac.compare_digest(supplied.lower(), expected)
  ```
</CodeGroup>

<Warning>
  Verify the raw bytes before parsing JSON. Re-serializing the body changes the signature.
</Warning>

## Delivery and retries

A `2xx` response marks the event delivered. Other responses, connection failures, or the 5-second timeout retry with exponential backoff for up to 8 attempts. Delivery is **at least once**, so the same event ID can arrive more than once. Separate events can arrive out of order; use each event's ID, creation time, and state rather than assuming request order.

Return a `2xx` quickly and move slow work to your own queue. Completed and exhausted delivery records are retained for 30 days.

## Inspect deliveries

Open [Webhooks](https://box.ascii.dev/box/dashboard?tab=webhooks) in the Box dashboard, then expand **Deliveries** beside an endpoint. The recent-delivery log shows what fired, the exact payload, Box and event IDs, attempt count, HTTP result or connection diagnostic, and the next retry time when applicable.
