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

# Events and webhooks

> Read your account's canvas events, or receive each one as a signed POST to your server.

Every organization has an event log, with one entry each time something happens on one of its canvases. Agents read the log with `list_events` or `wait_for_event`. Your server can also receive each event as a webhook.

| Who | Which events |
| - | - |
| An Admin's key or connected agent | Every canvas in the organization |
| A Member's key or connected agent | Canvases that Member can see: their own, and those shared with the organization |
| The webhook | Every canvas in the organization |

Only Admins can set up, test, rotate or remove the webhook. See [Organizations and members](https://docs.canvasview.dev/account/organizations-and-members.md#roles).

## Events

| Type | When |
| - | - |
| `answer.submitted` | Someone submitted an answer. |
| `state.changed` | Shared state changed, by a person or an agent. At most one per canvas every 5 seconds. |
| `change.requested` | A person asked the agent for a change in a comment. One per request. |
| `comment.added` | People commented, other than change requests. At most one per canvas every 5 seconds. |
| `canvas.closed` | An agent closed an active canvas. |
| `canvas.expired` | The canvas reached its expiry. |
| `canvas.deleted` | You or an agent deleted the canvas. A canvas deleted automatically after its read-only period writes no event; `canvas.expired` is its last. |
| `canvas.removed` | CanvasView removed the canvas after a report. |

An event looks like this, and carries nothing else:

```json theme={null}
{ "id": 42, "type": "answer.submitted", "canvasId": "0123456789abcdef0123456789abcdef", "at": 1762700000000 }
```

* `id` counts up by one for each event in your organization. A Member's agent can see gaps in `id` where events belong to canvases it cannot see.
* `at` is milliseconds since 1970.
* Events carry no answers, state values, comment text, names or emails. Fetch details with your API key, for example with `get_answers`, `get_canvas_state`, `list_comments` or `get_canvas`.

`state.changed` and `comment.added` mark the first change in a 5-second window. Later changes in the same window are not announced separately. Read the canvas a few seconds after the event to see them.

Events are kept for 30 days.

## Pull events

| Tool | REST | What it does |
| - | - | - |
| `list_events` | `GET /api/events?since=&canvasId=&limit=` | Returns events after `since`, oldest first, and `next`. Pass `next` as `since` on the next call to get each event once. `limit` is 1 to 200, default 50. |
| `wait_for_event` | `GET /api/events?since=&canvasId=&wait=` | Waits up to 60 seconds and returns as soon as an event after `since` exists, in the same shape as `list_events`, with empty `events` at the timeout. The tool takes `seconds`, default 15. Over REST, `wait` sets the seconds, and without it the call does not wait. Without `since`, it waits from now. |

Both are free.

An organization can hold 50 waits at once. Past that, a wait returns at once with whatever events exist, so pause briefly before calling again when a wait returns empty early.

## Set up a webhook

1. As an Admin, open [Webhooks](https://app.canvasview.dev/settings/webhooks).
2. Enter the **Endpoint URL**.
3. Select **Add endpoint**.
4. Copy the signing secret. It starts with `cvwhsec_` and is shown only once.

An organization has one endpoint. Deliveries start with events that happen after you save it.

The endpoint must use `https://` on port 443 or 8443 and a public host name. CanvasView refuses IP addresses, user names and passwords in the URL, `localhost`, and private names such as `.local` or `.internal`. It also refuses `canvasview.dev` addresses and names that carry an IP address, such as `nip.io`.

The Webhooks page shows the last success, the last failure and how many events are waiting to deliver. Select **Send test event** to send a test delivery, **Rotate secret** to get a new signing secret, or **Remove endpoint** to stop deliveries.

## Deliveries

Each event is sent as:

```http theme={null}
POST <your URL>
Content-Type: application/json
User-Agent: CanvasView-Webhooks/1.0
CanvasView-Signature: t=1762700000,v1=<hex signature>

{"id":42,"type":"answer.submitted","canvasId":"0123456789abcdef0123456789abcdef","at":1762700000000}
```

* Answer with any 2xx status within 10 seconds. The response body is ignored. Redirects are not followed and count as failures.
* Deliveries go out in order. A failed delivery is retried after 30 seconds, 2 minutes, 10 minutes and 1 hour, then hourly, for up to 24 hours. Later events wait behind it.
* A retried delivery carries the same `id`. Store the IDs you have handled and skip repeats.
* **Send test event** sends `{"id":0,"type":"webhook.test","canvasId":null,"at":...}`.

If no delivery succeeds for 24 hours, CanvasView switches the endpoint off, discards the events waiting to deliver, and sends you a [notification](https://docs.canvasview.dev/account/notifications.md). Events are still recorded, so agents can read them with `list_events`. Fix the endpoint, then save it again on the Webhooks page to turn deliveries back on. Deliveries restart with events that happen after you save. Read the ones you missed with `list_events`.

## Verify the signature

The `CanvasView-Signature` header holds `t`, a Unix time in seconds, and `v1`, an HMAC-SHA256 of `<t>.<raw body>` keyed with your signing secret, in hex.

1. Read the raw request body exactly as received.
2. Compute HMAC-SHA256 of `<t>.<body>` with your signing secret.
3. Compare it with `v1` in constant time.
4. Reject requests whose `t` is more than five minutes from now.

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCanvasView(rawBody, header, secret, toleranceSeconds = 300) {
  const fields = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=", 2)));
  const t = Number(fields.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds || !fields.v1) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(fields.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// Express:
// app.post("/canvasview", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyCanvasView(req.body.toString("utf8"), req.get("CanvasView-Signature"), process.env.CANVASVIEW_WEBHOOK_SECRET)) {
//     return res.sendStatus(400);
//   }
//   const event = JSON.parse(req.body);
//   res.sendStatus(204); // Then fetch the canvas details with your API key.
// });
```

The scheme matches Stripe's webhook signatures. Verification code written for the `Stripe-Signature` header works once it reads `CanvasView-Signature` instead.

## The signing secret

CanvasView generates the secret and shows it once, when you first save the endpoint or when you rotate it. Rotating takes effect at once: the next delivery uses the new secret. Keep the secret on your server only.

## Delivery safety

A delivery never follows a redirect, reads at most 4 KB of your response, and carries no API keys or cookies.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.