# REST hooks reference (/v1/hooks)

> Subscribe a URL to Workspace369 events with a personal access token, list and delete hooks, and fetch a sample event for a test trigger.

- Product: Workspace369
- Documentation version: 2026.3
- Section: Build with AI
- Last updated: 2026-11-06
- Canonical source: https://docs.workspace369.com/v/2026.3/build-with-ai/rest-hooks-reference/

---

REST hooks let an automation platform such as Zapier, Make or n8n subscribe to Workspace369 events with a personal access token, without anyone opening the app. The Workspace369 app for Zapier (beta) uses them to start a Zap instantly; see [Connect Workspace369 to Zapier](https://docs.workspace369.com/v/2026.3/build-with-ai/connect-zapier/). The platform creates a hook when a user turns its flow on and deletes it when they turn it off. Workspace369 then sends each event to the hook's URL, signed, exactly like a webhook made in **Settings → Webhooks**.

A hook is a webhook. It shows up in **Settings → Webhooks**, uses the same 27 events, the same signature headers and the same retries. See the [Webhooks reference](https://docs.workspace369.com/v/2026.3/build-with-ai/webhooks-reference/) for the payload, the signature check and retry behavior.

## Who can use it

- Every paid plan (Cadet and up), including the 14-day trial once the card is saved. A workspace with no plan gets `403` with `plan_required` when it creates, lists or samples hooks; it can still delete them. A webhook you add in **Settings → Webhooks** works on the same plans.
- An admin must turn on AI connections for the workspace (**Allow AI connections** under **AI connections**).
- A personal access token (it starts with `mcp_pat_`) with the **webhooks:manage** scope. Sign-in tokens from Claude or ChatGPT can't call these routes.
- You need the Manage webhooks permission yourself, which Managers and above have. A token never does more than your own role allows.
- Some events need more, because they carry people's details. A `lead.*` event needs access to Requests. A `booking.*` event needs access to the calendar and to every client's appointments. A `client.*` event needs you to see every client, not only your own. Owners and admins have all of these.

Hook calls are free: they don't count toward AI usage. They count toward the `/v1` rate limit of 120 requests a minute per token and 600 a minute per workspace.

## Create a token

1. Open **Settings**, then under **AI** choose **AI connections**.
2. Under **Personal access tokens**, choose **Create token**.
3. Enter a **Token name** that says what will use it, such as "Zapier".
4. Under **Permissions**, select the scopes for any actions the platform will also run, such as **clients:write**. Then, below them under **Automation triggers** ("Lets Zapier, Make and n8n start your automations when something happens."), select **webhooks:manage**.
5. Under **Expiry**, choose how long the token lasts, then choose **Create token**.
6. In the **Token created** dialog, copy the value and store it. It is not shown again.

See [Personal access tokens](https://docs.workspace369.com/v/2026.3/ai-connections/personal-access-tokens/) for expiry, review and revoking.

The token works in the workspace you had open when you created it. Every hook it creates belongs to that workspace.

## Base URL

`https://mcp.workspace369.com/v1`

Send the token on every call: `Authorization: Bearer mcp_pat_…`.

## Create a hook

`POST /v1/hooks`

```bash
curl -s https://mcp.workspace369.com/v1/hooks \
  -H "Authorization: Bearer $W369_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_url":"https://hooks.example.com/w369","events":["invoice.paid"]}'
```

| Field | Required | Notes |
| --- | --- | --- |
| `target_url` | Yes | An `https://` address on the standard port, with no username or password, on a public host. |
| `events` | Yes | One or more event names from the [event list](https://docs.workspace369.com/v/2026.3/build-with-ai/webhooks-reference/). |

The answer is `201`:

```json
{
  "id": "Wq3…",
  "target_url": "https://hooks.example.com/w369",
  "events": ["invoice.paid"],
  "active": true,
  "secret": "whsec_…",
  "reused": false
}
```

Store `secret` now. It is the signing secret for this hook, and it is returned only here.

If an active hook with the same `target_url` and the same events already exists, the answer is `200` with that hook, `"reused": true` and a **new** secret. This happens when a platform retries a subscribe. The answer also has a `message` saying so. The previous secret stops working, so always keep the latest one.

A workspace has a webhook limit, and it counts the webhooks made in the app too. Past it, the answer is `409` with `details.reason` set to `limit_reached` and `details.limit` holding the limit. Delete a hook you no longer use, then try again.

## List hooks

`GET /v1/hooks`

```bash
curl -s "https://mcp.workspace369.com/v1/hooks?target_url=https://hooks.example.com/w369" \
  -H "Authorization: Bearer $W369_TOKEN"
```

Returns `200` with `{"hooks":[…]}`. Each hook has `id`, `target_url`, `events`, `active`, `disabled_reason`, `created_at` and `last_delivery_at`. The secret is never listed.

The optional `target_url` filter matches the address exactly. Use it to check whether a hook for your URL already exists.

`active` is `false` when the hook was switched off, and `disabled_reason` says why, for example when its address can't be reached safely.

## Get a sample event

`GET /v1/hooks/sample?event=invoice.paid`

Platforms use this for their "test trigger" step, so a user can map fields before a real event happens.

- For the workspace owner and admins, the answer is the most recent real delivery of that event from the last 30 days, with `"sample": false` and `delivered_at`.
- Otherwise, or when there is no recent delivery, the answer is a made-up event with `"sample": true`. It has the same envelope and field names as a real one, with placeholder values.

```bash
curl -s "https://mcp.workspace369.com/v1/hooks/sample?event=invoice.paid" \
  -H "Authorization: Bearer $W369_TOKEN"
```

The answer is `200`. It is the event envelope from the [Webhooks reference](https://docs.workspace369.com/v/2026.3/build-with-ai/webhooks-reference/), with `sample` added, and `delivered_at` (milliseconds since 1970) when it is a real delivery:

```json
{
  "sample": true,
  "webhook_id": "…",
  "event_type": "invoice.paid",
  "entity_id": "sample_invoice",
  "workspace_id": "…",
  "subscription_id": "…",
  "data": { "id": "sample_invoice", "is_paid": true, "invoice_status": "Paid" }
}
```

An unknown event name returns `422` with `invalid_request` and the list of valid names. A `lead.*`, `booking.*` or `client.*` sample needs the same access as subscribing to that event does; without it the answer is `403` with `forbidden`.

## Delete a hook

`DELETE /v1/hooks/{id}`

```bash
curl -s -X DELETE https://mcp.workspace369.com/v1/hooks/Wq3… \
  -H "Authorization: Bearer $W369_TOKEN"
```

Returns `204`, also when the hook is already gone. Events stop at once. A platform retrying an unsubscribe is never told it failed.

A workspace that has dropped to no plan can still delete its hooks, through the API or in **Settings → Webhooks**, so turning off a Zap still removes its hook. Its hooks keep sending events while it has any plan.

## Signature headers

Every event sent to a hook is signed with the hook's `secret`, exactly like an event sent to a webhook made in the app:

| Header | Value |
| --- | --- |
| `X-Webhook-Id` | A unique id for this event. The same event has the same id on every retry. |
| `X-Webhook-Timestamp` | The send time in milliseconds since 1970, as text. |
| `X-Webhook-Signature` | The hex HMAC-SHA256 of the raw request body followed by the timestamp text, using the whole `secret` (with its `whsec_` prefix) as the key. |

Compute it over the raw bytes you received, and reject an event whose timestamp is more than five minutes from now. Each retry is signed again with a fresh timestamp. The [Webhooks reference](https://docs.workspace369.com/v/2026.3/build-with-ai/webhooks-reference/#verify-the-signature) has a Node.js example.

## Errors

Every error has the shape `{"error":{"code","message","details"}}`.

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthorized` | Missing or invalid token. |
| 403 | `insufficient_scope` | The token lacks **webhooks:manage**; `details.required_scope` names it. |
| 403 | `forbidden` | You don't have the Manage webhooks permission, the event needs access you lack (Requests, the calendar or every client), or AI connections are off (`details.reason` is `mcp_not_enabled`). |
| 403 | `plan_required` | The workspace has no plan (create, list and sample only; delete still works). `details.required_tier_name` names the plan needed. |
| 409 | `conflict` | The workspace is at its webhook limit (`details.reason` is `limit_reached`). |
| 422 | `invalid_request` | The URL isn't allowed, an event name is unknown, or the id isn't valid. `message` says which. |
| 429 | `rate_limited` | Too many requests. Wait for `Retry-After` seconds. |
| 503 | `tool_disabled` | An admin has switched this action off for AI connections. |
| 503 | `service_unavailable` | A temporary problem, such as a DNS check that didn't answer. Try again. |

## Check that it works

1. Create a hook for `invoice.paid` pointing at a request inspector you control.
2. Call `GET /v1/hooks/sample?event=invoice.paid` and confirm the fields you need are there.
3. Open **Settings → Webhooks** in the app and confirm the hook is listed.
4. Mark a test invoice paid and confirm the delivery arrives with an `X-Webhook-Signature` header that verifies with the hook's secret.
5. Delete the hook and confirm it is gone from **Settings → Webhooks**.

## Related

- [Connect Workspace369 to Zapier](https://docs.workspace369.com/v/2026.3/build-with-ai/connect-zapier/): the Workspace369 app for Zapier (beta) subscribes with these routes.
- [Webhooks reference](https://docs.workspace369.com/v/2026.3/build-with-ai/webhooks-reference/): the events, payloads, retries and recent deliveries.
- [Call Workspace369 over HTTP (Builder API)](https://docs.workspace369.com/v/2026.3/build-with-ai/call-workspace369-over-http-builder-api/): the rest of `/v1`.
