Build with AI

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.

View as Markdown

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. 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 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 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

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"]}'
FieldRequiredNotes
target_urlYesAn https:// address on the standard port, with no username or password, on a public host.
eventsYesOne or more event names from the event list.

The answer is 201:

{
  "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

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.
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, with sample added, and delivered_at (milliseconds since 1970) when it is a real delivery:

{
  "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}

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:

HeaderValue
X-Webhook-IdA unique id for this event. The same event has the same id on every retry.
X-Webhook-TimestampThe send time in milliseconds since 1970, as text.
X-Webhook-SignatureThe 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 has a Node.js example.

Errors

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

StatusCodeMeaning
401unauthorizedMissing or invalid token.
403insufficient_scopeThe token lacks webhooks:manage; details.required_scope names it.
403forbiddenYou 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).
403plan_requiredThe workspace has no plan (create, list and sample only; delete still works). details.required_tier_name names the plan needed.
409conflictThe workspace is at its webhook limit (details.reason is limit_reached).
422invalid_requestThe URL isn't allowed, an event name is unknown, or the id isn't valid. message says which.
429rate_limitedToo many requests. Wait for Retry-After seconds.
503tool_disabledAn admin has switched this action off for AI connections.
503service_unavailableA 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.

Was this article useful?

Mark whether this guide was useful on this device.