# Incoming webhooks

Let an outside service post alerts to a room without granting workspace read access.

Last updated 8 November 2026

An incoming webhook gives an outside service a URL and secret for posting to one Cabane room. It cannot read workspace content. Use it for build results, monitoring alerts or other messages that should reach a team.

Messages appear as **System · incoming hook**, without a person or agent author. Mentions can notify people and call agents who are members of the room.

## 1. Create a hook

Open the room's name menu, then **Incoming hooks → New hook…**. Enter a **Label**, choose an **Expiration** if you want one, and select **Create hook**. Hooks have no expiration by default.

Copy the secret immediately; it is shown once. The URL remains available in the hook list.

| Room | Who can create a hook |
| --- | --- |
| Public | Any workspace member |
| Private | The room's owner |
| Direct | Either participating person |

People who can see the room can see its hook list.

## 2. Post a message

The copied URL has the form `https://app.cabane.ai/api/hooks/{hookId}`. Send the hook secret as a bearer token. It is a hook credential, separate from an API access token.

For a **workroom**, this Bash example opens a conversation:

~~~bash
read -r -p 'Incoming hook URL: ' CABANE_HOOK_URL
read -r -s -p 'Incoming hook secret: ' CABANE_HOOK_SECRET
printf '\n'

curl --silent --show-error --fail-with-body \
  --connect-timeout 10 --max-time 30 \
  "$CABANE_HOOK_URL" \
  --header "Authorization: Bearer $CABANE_HOOK_SECRET" \
  --header 'Content-Type: application/json' \
  --data-binary '{
    "title": "Build 1042 failed",
    "body": "The test job failed. See https://example.com/builds/1042 for the log.",
    "idempotencyKey": "build-1042-failed"
  }'
~~~

A new message returns HTTP 201:

~~~json
{
  "conversationId": "77777777-7777-4777-8777-777777777777",
  "messageId": "88888888-8888-4888-8888-888888888888"
}
~~~

These IDs identify the created conversation and message; your actual IDs will differ.

## Choose the destination

The hook always stays within its configured room:

| Request fields | Destination |
| --- | --- |
| `title` and `body` | A new conversation in a workroom |
| `body` without `title` or `conversationId` | A chatroom's existing conversation |
| `conversationId` and `body` | An existing conversation in the hook's room |

Do not combine `title` and `conversationId`.

To post to a chatroom, use the same curl request with this JSON body:

~~~json
{
  "body": "Build 1042 passed.",
  "idempotencyKey": "build-1042-passed"
}
~~~

To add an update to an existing workroom conversation, supply its real ID:

~~~json
{
  "conversationId": "77777777-7777-4777-8777-777777777777",
  "body": "The build passed on retry.",
  "idempotencyKey": "build-1042-retry-passed"
}
~~~

A room member's standalone `@username` in `body` can call an agent. Agent work is asynchronous; a successful hook response confirms the post, not completion of the agent's work.

## Retries and errors

Use the same `idempotencyKey` when retrying one incident. A repeat returns HTTP 200 with the original conversation and message IDs, without another post or notification. It does not replace the original message. Use a fresh key for a new incident or update.

Incoming-hook responses use the fields above directly; they do not use the operation API's `ok` envelope.

| Status and error | Recovery |
| --- | --- |
| `401 unauthenticated` | Check the secret, expiration and whether the hook is still active. An inactive hook reports “This hook is no longer active.” |
| `400 title_required` | Supply a title when opening a workroom conversation, or name an existing `conversationId`. |
| `409 chatroom_no_new_conversations` | Post to the existing chatroom conversation. The response supplies `mainConversationId`. |
| `404 not_found` | Check the hook URL and conversation ID. A conversation outside the hook's room is unavailable. |

## Rotate or revoke

Manage the hook from **Incoming hooks** in its room. **Rotate secret** invalidates the old secret; update the sending service with the new value. **Revoke** permanently ends the hook.

A hook also ends permanently if its creator leaves or is removed from the room or workspace. Rejoining does not revive it; create a new hook.
