Ndoto Docs

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:

FieldDescription
System PromptInstructions that shape the bot's tone, scope, and behaviour
TemperatureControls response creativity (0 = precise, 1 = more varied)
Max Exchanges Before HandoffHow many back-and-forth messages before the bot escalates to a human
Handoff KeywordsPhrases a customer can say to trigger immediate human handoff (e.g. "speak to agent")

Creating a Bot

  1. Go to Settings → Agent Bots
  2. Click Add Bot
  3. Choose a bot type: Webhook or AI Agent
  4. For webhook bots: enter a name and a valid webhook URL (https://...)
  5. For AI agent bots: configure the system prompt and handoff rules
  6. 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

  1. Go to Settings → Inboxes
  2. Click Settings on the inbox you want to configure
  3. Go to the Configuration tab
  4. Under Select an agent bot, pick the bot from the dropdown
  5. 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 reply

Webhook 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}/messages

Headers:

api_access_token: YOUR_BOT_ACCESS_TOKEN
Content-Type: application/json

Body:

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

Sending 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

EventDescription
message_createdNew message in a conversation
message_updatedA message was edited
conversation_openedA resolved conversation was reopened
conversation_resolvedA conversation was resolved
webwidget_triggeredA web widget session started

Tips

  • Always return HTTP 200 immediately — 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: true messages 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.


On this page