Agent Bots — Webhook & AI Automation in Ndoto
Automate conversations in Ndoto using webhook bots or built-in AI agents. Learn how to create, configure, and assign agent bots to inboxes with real code examples.
Agent Bots
Agent bots automate customer conversations so your team can focus on the conversations that need a human touch. You can build a custom webhook bot that integrates with any external service, or use the built-in AI agent powered by a knowledge base.
Manage your bots from Settings → Agent Bots. Create a new bot using the Add Bot button.
Bot Types
Webhook Bot
A webhook bot receives conversation events via HTTP POST to a URL you provide. Your server handles the event and can reply back to the conversation using the Ndoto API. This gives you full control — connect any AI model, CRM, database, or custom logic.
AI Agent Bot
The AI agent bot auto-responds to incoming messages using your knowledge base and a configurable system prompt. No external server required — it runs inside Ndoto.
Configuration options:
| Field | Description |
|---|---|
| System Prompt | Instructions that shape the bot's tone, scope, and behaviour |
| Temperature | Controls response creativity (0 = precise, 1 = more varied) |
| Max Exchanges Before Handoff | How many back-and-forth messages before the bot escalates to a human |
| Handoff Keywords | Phrases a customer can say to trigger immediate human handoff (e.g. "speak to agent") |
Creating a Bot
- Go to Settings → Agent Bots
- Click Add Bot
- Choose a bot type: Webhook or AI Agent
- For webhook bots: enter a name and a valid webhook URL (
https://...) - For AI agent bots: configure the system prompt and handoff rules
- Click Create Bot
After creation, an access token is shown once. Copy it and store it securely — it authenticates incoming API calls from your bot back to Ndoto.
Assigning a Bot to an Inbox
- Go to Settings → Inboxes
- Click Settings on the inbox you want to configure
- Go to the Configuration tab
- Under Select an agent bot, pick the bot from the dropdown
- Click Update
One bot can be assigned to multiple inboxes. An inbox can only have one active bot at a time.
Webhook Bot — Developer Guide
How it works
When a conversation event occurs in an inbox that has your bot assigned, Ndoto sends an HTTP POST request to your webhook URL with a JSON payload. Your server processes it and can reply using the Ndoto REST API.
Customer sends message
↓
Ndoto fires POST to your webhook URL
↓
Your server processes the message
↓
Your server calls Ndoto API to send a reply
↓
Customer sees the replyWebhook Payload — message_created
This is the most common event. Fired every time a new message arrives in a conversation assigned to your bot.
{
"event": "message_created",
"id": 1234,
"content": "Hello, I need help with my order",
"content_type": "text",
"message_type": "incoming",
"created_at": 1741526400,
"private": false,
"source_id": null,
"additional_attributes": {},
"content_attributes": {},
"sender": {
"id": 56,
"name": "Jane Doe",
"email": "[email protected]",
"phone_number": "+260966123456",
"avatar": "https://app.usendoto.com/rails/active_storage/...",
"identifier": null,
"type": "contact"
},
"inbox": {
"id": 3,
"name": "Website Chat",
"channel": "Channel::WebWidget"
},
"conversation": {
"id": 99,
"account_id": 1,
"status": "open",
"assignee": null,
"meta": {
"sender": {
"id": 56,
"name": "Jane Doe",
"type": "contact"
},
"channel": "Channel::WebWidget"
}
},
"account": {
"id": 1,
"name": "Acme Support"
}
}Webhook Payload — conversation_resolved
{
"event": "conversation_resolved",
"id": 99,
"status": "resolved",
"account_id": 1,
"meta": {
"sender": {
"id": 56,
"name": "Jane Doe",
"type": "contact"
},
"channel": "Channel::WebWidget"
},
"account": {
"id": 1,
"name": "Acme Support"
}
}Sending a Reply
To reply to a message, call the Ndoto API using the bot's access token and the conversation ID from the payload.
POST https://app.usendoto.com/api/v1/accounts/{account_id}/conversations/{conversation_id}/messagesHeaders:
api_access_token: YOUR_BOT_ACCESS_TOKEN
Content-Type: application/jsonBody:
{
"content": "Hi Jane! I can help with your order. Can you share your order number?",
"message_type": "outgoing",
"private": false
}Complete Example — Node.js (Express)
A minimal webhook bot that receives messages and replies using the Ndoto API.
import express from 'express';
import axios from 'axios';
const app = express();
app.use(express.json());
const NDOTO_BASE_URL = 'https://app.usendoto.com';
const BOT_ACCESS_TOKEN = 'your_bot_access_token_here';
app.post('/webhook', async (req, res) => {
const { event, content, conversation, account } = req.body;
// Only handle incoming customer messages
if (event !== 'message_created') {
return res.sendStatus(200);
}
// Ignore messages sent by the bot itself (outgoing)
if (req.body.message_type === 'outgoing') {
return res.sendStatus(200);
}
const conversationId = conversation.id;
const accountId = account.id;
try {
await axios.post(
`${NDOTO_BASE_URL}/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
{
content: `You said: "${content}". How can I help you further?`,
message_type: 'outgoing',
private: false,
},
{
headers: {
api_access_token: BOT_ACCESS_TOKEN,
'Content-Type': 'application/json',
},
}
);
} catch (error) {
console.error('Failed to send reply:', error.message);
}
res.sendStatus(200);
});
app.listen(3000, () => console.log('Bot webhook listening on port 3000'));Complete Example — Python (Flask)
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
NDOTO_BASE_URL = "https://app.usendoto.com"
BOT_ACCESS_TOKEN = "your_bot_access_token_here"
@app.route("/webhook", methods=["POST"])
def webhook():
data = request.json
# Only handle incoming customer messages
if data.get("event") != "message_created":
return "", 200
if data.get("message_type") == "outgoing":
return "", 200
conversation_id = data["conversation"]["id"]
account_id = data["account"]["id"]
customer_message = data.get("content", "")
reply = f'Thanks for reaching out! You said: "{customer_message}". Our team will get back to you shortly.'
requests.post(
f"{NDOTO_BASE_URL}/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages",
json={
"content": reply,
"message_type": "outgoing",
"private": False,
},
headers={
"api_access_token": BOT_ACCESS_TOKEN,
"Content-Type": "application/json",
},
)
return "", 200
if __name__ == "__main__":
app.run(port=3000)Complete Example — Ruby (Sinatra)
require 'sinatra'
require 'json'
require 'net/http'
require 'uri'
NDOTO_BASE_URL = 'https://app.usendoto.com'
BOT_ACCESS_TOKEN = 'your_bot_access_token_here'
post '/webhook' do
payload = JSON.parse(request.body.read)
# Only handle incoming customer messages
return status 200 unless payload['event'] == 'message_created'
return status 200 if payload['message_type'] == 'outgoing'
conversation_id = payload['conversation']['id']
account_id = payload['account']['id']
content = payload['content']
uri = URI("#{NDOTO_BASE_URL}/api/v1/accounts/#{account_id}/conversations/#{conversation_id}/messages")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['api_access_token'] = BOT_ACCESS_TOKEN
request['Content-Type'] = 'application/json'
request.body = {
content: "Thanks! You said: \"#{content}\". We'll be with you shortly.",
message_type: 'outgoing',
private: false
}.to_json
http.request(request)
status 200
endSending Private Notes
Private notes are visible only to agents — not the customer. Useful for logging, tagging, or leaving context for human agents.
await axios.post(
`${NDOTO_BASE_URL}/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
{
content: 'Bot could not resolve this query. Customer is asking about order #4521.',
message_type: 'outgoing',
private: true, // 👈 only agents see this
},
{ headers: { api_access_token: BOT_ACCESS_TOKEN } }
);Resolving a Conversation
To mark a conversation as resolved after the bot handles it:
await axios.patch(
`${NDOTO_BASE_URL}/api/v1/accounts/${accountId}/conversations`,
{ id: conversationId, status: 'resolved' },
{ headers: { api_access_token: BOT_ACCESS_TOKEN } }
);Assigning to a Human Agent
To hand off a conversation to a human agent:
await axios.patch(
`${NDOTO_BASE_URL}/api/v1/accounts/${accountId}/conversations/${conversationId}/assignments`,
{ assignee_id: agentUserId },
{ headers: { api_access_token: BOT_ACCESS_TOKEN } }
);All Webhook Events
| Event | Description |
|---|---|
message_created | New message in a conversation |
message_updated | A message was edited |
conversation_opened | A resolved conversation was reopened |
conversation_resolved | A conversation was resolved |
webwidget_triggered | A web widget session started |
Tips
- Always return HTTP
200immediately — Ndoto retries if your server takes too long or returns an error - Check
message_type !== 'outgoing'to avoid infinite loops where your bot replies to its own messages - Store the bot access token in an environment variable, never hardcode it
- Use
private: truemessages to leave notes for human agents without the customer seeing them
System Bots
Bots with a System badge are globally available across all accounts and are managed by a platform administrator. They cannot be edited or deleted from the account settings.
Frequently Asked Questions
Can a bot and a human agent handle the same inbox? Yes. When a bot is assigned to an inbox, it handles conversations automatically. A human agent can take over any conversation — once assigned, the bot stops responding to that conversation.
How do I avoid the bot replying to its own messages?
Check the message_type field in the payload. Incoming messages from customers have message_type: "incoming". Your bot's replies will have message_type: "outgoing" — ignore those to avoid infinite loops.
Where do I find the bot's access token after creation? Go to Settings → Agent Bots, click the edit icon on the bot, then use Reset Access Token to generate a new one. The original token is only shown once at creation time.
Can I assign a bot to a specific conversation instead of an entire inbox? Yes. When viewing a conversation, you can assign a bot directly to it from the conversation details panel. This overrides the inbox-level bot assignment for that conversation.
Does the webhook receive messages from the bot itself?
Yes — all messages fire the message_created event. Always filter by message_type === "incoming" to only process customer messages.
Related Docs
Ndoto Product Guide — Features & How They Work
A complete guide to Ndoto's product features — conversations, channels, Akili AI, automation, contacts, Help Center, reports, and teams. Learn how every part of Ndoto works.
How AI Credits Work in Akili
Understand how Akili AI credits are consumed, how billing works, what each AI action costs, and what happens when credits run out.