> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.apologist.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.apologist.ai/_mcp/server.

# Chatwoot Agent Bot Webhook

POST https://your-agent-domain.com/api/v1/channels/{id}/chatwoot
Content-Type: application/json

Receives Chatwoot Agent Bot webhook events for the channel. Chatwoot owns the messaging inbox (Facebook, website widget, and others). This Agent replies through the Chatwoot API and maps native bot handoff to conversation pause/resume. Requests are verified via the `X-Chatwoot-Signature` HMAC-SHA256 header using the configured webhook secret unless an `api_key` is present and no secret is set. The route acknowledges immediately (Chatwoot times out in about 5 seconds) and processes events asynchronously.

Reference: https://docs.apologist.ai/agent-api/api-reference/channels/receive-chatwoot-webhook

## Request

### Path parameters

- `id` (string, required) — The channel id

### Headers

- `X-Chatwoot-Signature` (string, optional) — `sha256=` plus hex HMAC-SHA256 of `{timestamp}.{rawBody}` keyed with the Agent Bot webhook secret. Required when the webhook URL does not include an api\_key, and whenever a webhook secret is configured.
- `X-Chatwoot-Timestamp` (string, optional) — Unix timestamp used in the HMAC payload.

### Body (application/json)

This endpoint expects an object.

- `event` (string, optional) — Chatwoot event name. Incoming visitor messages use `message_created`. Staff takeover and assignment use conversation events such as `conversation_opened`, `conversation_status_changed`, and `conversation_updated`.
- `id` (integer, optional) — Message id on `message_*` events. Conversation display id on `conversation_*` events.
- `content` (string, optional, nullable) — Message text. Present on message events. Empty or private notes are ignored.
- `message_type` (string, optional) — Incoming visitor messages are `incoming` (or `0`). Staff replies are `outgoing` (or `1`).
- `private` (boolean, optional) — When true, the Agent ignores the message (Chatwoot private note).
- `account` (object, optional)
  - `id` (integer, optional) — Must match the channel Account ID, or the request is rejected.
- `sender` (object, optional) — Present on message events.
  - `id` (integer, optional)
  - `name` (string, optional)
  - `type` (string, optional) — `contact` for visitors, `user` for staff, `agent_bot` for this Agent. The Agent ignores its own outgoing messages.
- `conversation` (object, optional) — Nested on message events. Conversation events send `id`, `status`, and `meta` at the top level instead.
  - `id` (integer, optional) — Conversation display id.
  - `status` (string, optional) — `pending`, `open`, `resolved`, or `snoozed`.
  - `account` (object, optional)
    - `id` (integer, optional)
  - `meta` (object, optional)
    - `assignee_type` (string, optional) — `AgentBot` while the Agent owns the thread. `User` after staff take over.
    - `sender` (object, optional)
      - `id` (integer, optional)
      - `name` (string, optional)
- `status` (string, optional) — Top-level conversation status on conversation events.
- `meta` (object, optional) — Top-level conversation meta on conversation events.
  - `assignee_type` (string, optional) — `AgentBot` while the Agent owns the thread. `User` after staff take over.
  - `sender` (object, optional)
    - `id` (integer, optional)
    - `name` (string, optional)

## Response

### 200

Event acknowledged

## Errors

### 400 Bad Request Error

Empty or invalid body

- `any`

### 403 Forbidden Error

Invalid signature or account mismatch

- `any`

### 503 Service Unavailable Error

Service Unavailable

- `any`

## Examples

### Incoming Visitor Message

**Request**

```json
{
  "event": "message_created",
  "id": 9001,
  "content": "Hello from Messenger",
  "message_type": "incoming",
  "private": false,
  "account": {
    "id": 7
  },
  "sender": {
    "id": 55,
    "name": "Pat",
    "type": "contact"
  },
  "conversation": {
    "id": 42,
    "status": "pending",
    "account": {
      "id": 7
    },
    "meta": {
      "assignee_type": "AgentBot",
      "sender": {
        "id": 55,
        "name": "Pat"
      }
    }
  }
}
```

**SDK Code**

```python Incoming Visitor Message
import requests

url = "https://your-agent-domain.com/api/v1/channels/:id/chatwoot"

payload = {
    "event": "message_created",
    "id": 9001,
    "content": "Hello from Messenger",
    "message_type": "incoming",
    "private": False,
    "account": { "id": 7 },
    "sender": {
        "id": 55,
        "name": "Pat",
        "type": "contact"
    },
    "conversation": {
        "id": 42,
        "status": "pending",
        "account": { "id": 7 },
        "meta": {
            "assignee_type": "AgentBot",
            "sender": {
                "id": 55,
                "name": "Pat"
            }
        }
    }
}
headers = {
    "X-Chatwoot-Signature": "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
    "X-Chatwoot-Timestamp": "1710000000",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript Incoming Visitor Message
const url = 'https://your-agent-domain.com/api/v1/channels/:id/chatwoot';
const options = {
  method: 'POST',
  headers: {
    'X-Chatwoot-Signature': 'sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
    'X-Chatwoot-Timestamp': '1710000000',
    'Content-Type': 'application/json'
  },
  body: '{"event":"message_created","id":9001,"content":"Hello from Messenger","message_type":"incoming","private":false,"account":{"id":7},"sender":{"id":55,"name":"Pat","type":"contact"},"conversation":{"id":42,"status":"pending","account":{"id":7},"meta":{"assignee_type":"AgentBot","sender":{"id":55,"name":"Pat"}}}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Incoming Visitor Message
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://your-agent-domain.com/api/v1/channels/:id/chatwoot"

	payload := strings.NewReader("{\n  \"event\": \"message_created\",\n  \"id\": 9001,\n  \"content\": \"Hello from Messenger\",\n  \"message_type\": \"incoming\",\n  \"private\": false,\n  \"account\": {\n    \"id\": 7\n  },\n  \"sender\": {\n    \"id\": 55,\n    \"name\": \"Pat\",\n    \"type\": \"contact\"\n  },\n  \"conversation\": {\n    \"id\": 42,\n    \"status\": \"pending\",\n    \"account\": {\n      \"id\": 7\n    },\n    \"meta\": {\n      \"assignee_type\": \"AgentBot\",\n      \"sender\": {\n        \"id\": 55,\n        \"name\": \"Pat\"\n      }\n    }\n  }\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-Chatwoot-Signature", "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
	req.Header.Add("X-Chatwoot-Timestamp", "1710000000")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Incoming Visitor Message
require 'uri'
require 'net/http'

url = URI("https://your-agent-domain.com/api/v1/channels/:id/chatwoot")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-Chatwoot-Signature"] = 'sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
request["X-Chatwoot-Timestamp"] = '1710000000'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"event\": \"message_created\",\n  \"id\": 9001,\n  \"content\": \"Hello from Messenger\",\n  \"message_type\": \"incoming\",\n  \"private\": false,\n  \"account\": {\n    \"id\": 7\n  },\n  \"sender\": {\n    \"id\": 55,\n    \"name\": \"Pat\",\n    \"type\": \"contact\"\n  },\n  \"conversation\": {\n    \"id\": 42,\n    \"status\": \"pending\",\n    \"account\": {\n      \"id\": 7\n    },\n    \"meta\": {\n      \"assignee_type\": \"AgentBot\",\n      \"sender\": {\n        \"id\": 55,\n        \"name\": \"Pat\"\n      }\n    }\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java Incoming Visitor Message
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://your-agent-domain.com/api/v1/channels/:id/chatwoot")
  .header("X-Chatwoot-Signature", "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
  .header("X-Chatwoot-Timestamp", "1710000000")
  .header("Content-Type", "application/json")
  .body("{\n  \"event\": \"message_created\",\n  \"id\": 9001,\n  \"content\": \"Hello from Messenger\",\n  \"message_type\": \"incoming\",\n  \"private\": false,\n  \"account\": {\n    \"id\": 7\n  },\n  \"sender\": {\n    \"id\": 55,\n    \"name\": \"Pat\",\n    \"type\": \"contact\"\n  },\n  \"conversation\": {\n    \"id\": 42,\n    \"status\": \"pending\",\n    \"account\": {\n      \"id\": 7\n    },\n    \"meta\": {\n      \"assignee_type\": \"AgentBot\",\n      \"sender\": {\n        \"id\": 55,\n        \"name\": \"Pat\"\n      }\n    }\n  }\n}")
  .asString();
```

```php Incoming Visitor Message
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://your-agent-domain.com/api/v1/channels/:id/chatwoot', [
  'body' => '{
  "event": "message_created",
  "id": 9001,
  "content": "Hello from Messenger",
  "message_type": "incoming",
  "private": false,
  "account": {
    "id": 7
  },
  "sender": {
    "id": 55,
    "name": "Pat",
    "type": "contact"
  },
  "conversation": {
    "id": 42,
    "status": "pending",
    "account": {
      "id": 7
    },
    "meta": {
      "assignee_type": "AgentBot",
      "sender": {
        "id": 55,
        "name": "Pat"
      }
    }
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-Chatwoot-Signature' => 'sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
    'X-Chatwoot-Timestamp' => '1710000000',
  ],
]);

echo $response->getBody();
```

```csharp Incoming Visitor Message
using RestSharp;

var client = new RestClient("https://your-agent-domain.com/api/v1/channels/:id/chatwoot");
var request = new RestRequest(Method.POST);
request.AddHeader("X-Chatwoot-Signature", "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
request.AddHeader("X-Chatwoot-Timestamp", "1710000000");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"event\": \"message_created\",\n  \"id\": 9001,\n  \"content\": \"Hello from Messenger\",\n  \"message_type\": \"incoming\",\n  \"private\": false,\n  \"account\": {\n    \"id\": 7\n  },\n  \"sender\": {\n    \"id\": 55,\n    \"name\": \"Pat\",\n    \"type\": \"contact\"\n  },\n  \"conversation\": {\n    \"id\": 42,\n    \"status\": \"pending\",\n    \"account\": {\n      \"id\": 7\n    },\n    \"meta\": {\n      \"assignee_type\": \"AgentBot\",\n      \"sender\": {\n        \"id\": 55,\n        \"name\": \"Pat\"\n      }\n    }\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Incoming Visitor Message
import Foundation

let headers = [
  "X-Chatwoot-Signature": "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
  "X-Chatwoot-Timestamp": "1710000000",
  "Content-Type": "application/json"
]
let parameters = [
  "event": "message_created",
  "id": 9001,
  "content": "Hello from Messenger",
  "message_type": "incoming",
  "private": false,
  "account": ["id": 7],
  "sender": [
    "id": 55,
    "name": "Pat",
    "type": "contact"
  ],
  "conversation": [
    "id": 42,
    "status": "pending",
    "account": ["id": 7],
    "meta": [
      "assignee_type": "AgentBot",
      "sender": [
        "id": 55,
        "name": "Pat"
      ]
    ]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://your-agent-domain.com/api/v1/channels/:id/chatwoot")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Staff Takeover

**Request**

```json
{
  "event": "conversation_opened",
  "id": 42,
  "account": {
    "id": 7
  },
  "status": "open",
  "meta": {
    "assignee_type": "User",
    "sender": {
      "id": 55,
      "name": "Pat"
    }
  }
}
```

**SDK Code**

```python Staff Takeover
import requests

url = "https://your-agent-domain.com/api/v1/channels/:id/chatwoot"

payload = {
    "event": "conversation_opened",
    "id": 42,
    "account": { "id": 7 },
    "status": "open",
    "meta": {
        "assignee_type": "User",
        "sender": {
            "id": 55,
            "name": "Pat"
        }
    }
}
headers = {
    "X-Chatwoot-Signature": "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
    "X-Chatwoot-Timestamp": "1710000000",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript Staff Takeover
const url = 'https://your-agent-domain.com/api/v1/channels/:id/chatwoot';
const options = {
  method: 'POST',
  headers: {
    'X-Chatwoot-Signature': 'sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
    'X-Chatwoot-Timestamp': '1710000000',
    'Content-Type': 'application/json'
  },
  body: '{"event":"conversation_opened","id":42,"account":{"id":7},"status":"open","meta":{"assignee_type":"User","sender":{"id":55,"name":"Pat"}}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Staff Takeover
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://your-agent-domain.com/api/v1/channels/:id/chatwoot"

	payload := strings.NewReader("{\n  \"event\": \"conversation_opened\",\n  \"id\": 42,\n  \"account\": {\n    \"id\": 7\n  },\n  \"status\": \"open\",\n  \"meta\": {\n    \"assignee_type\": \"User\",\n    \"sender\": {\n      \"id\": 55,\n      \"name\": \"Pat\"\n    }\n  }\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-Chatwoot-Signature", "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
	req.Header.Add("X-Chatwoot-Timestamp", "1710000000")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Staff Takeover
require 'uri'
require 'net/http'

url = URI("https://your-agent-domain.com/api/v1/channels/:id/chatwoot")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-Chatwoot-Signature"] = 'sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
request["X-Chatwoot-Timestamp"] = '1710000000'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"event\": \"conversation_opened\",\n  \"id\": 42,\n  \"account\": {\n    \"id\": 7\n  },\n  \"status\": \"open\",\n  \"meta\": {\n    \"assignee_type\": \"User\",\n    \"sender\": {\n      \"id\": 55,\n      \"name\": \"Pat\"\n    }\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java Staff Takeover
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://your-agent-domain.com/api/v1/channels/:id/chatwoot")
  .header("X-Chatwoot-Signature", "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
  .header("X-Chatwoot-Timestamp", "1710000000")
  .header("Content-Type", "application/json")
  .body("{\n  \"event\": \"conversation_opened\",\n  \"id\": 42,\n  \"account\": {\n    \"id\": 7\n  },\n  \"status\": \"open\",\n  \"meta\": {\n    \"assignee_type\": \"User\",\n    \"sender\": {\n      \"id\": 55,\n      \"name\": \"Pat\"\n    }\n  }\n}")
  .asString();
```

```php Staff Takeover
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://your-agent-domain.com/api/v1/channels/:id/chatwoot', [
  'body' => '{
  "event": "conversation_opened",
  "id": 42,
  "account": {
    "id": 7
  },
  "status": "open",
  "meta": {
    "assignee_type": "User",
    "sender": {
      "id": 55,
      "name": "Pat"
    }
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-Chatwoot-Signature' => 'sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
    'X-Chatwoot-Timestamp' => '1710000000',
  ],
]);

echo $response->getBody();
```

```csharp Staff Takeover
using RestSharp;

var client = new RestClient("https://your-agent-domain.com/api/v1/channels/:id/chatwoot");
var request = new RestRequest(Method.POST);
request.AddHeader("X-Chatwoot-Signature", "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
request.AddHeader("X-Chatwoot-Timestamp", "1710000000");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"event\": \"conversation_opened\",\n  \"id\": 42,\n  \"account\": {\n    \"id\": 7\n  },\n  \"status\": \"open\",\n  \"meta\": {\n    \"assignee_type\": \"User\",\n    \"sender\": {\n      \"id\": 55,\n      \"name\": \"Pat\"\n    }\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Staff Takeover
import Foundation

let headers = [
  "X-Chatwoot-Signature": "sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
  "X-Chatwoot-Timestamp": "1710000000",
  "Content-Type": "application/json"
]
let parameters = [
  "event": "conversation_opened",
  "id": 42,
  "account": ["id": 7],
  "status": "open",
  "meta": [
    "assignee_type": "User",
    "sender": [
      "id": 55,
      "name": "Pat"
    ]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://your-agent-domain.com/api/v1/channels/:id/chatwoot")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```