> ## Documentation Index
> Fetch the complete documentation index at: https://www.text.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a custom bot

> Create an automated agent that greets customers, handles common questions, and hands off to a human when needed.

**APIs involved:** [Configuration API](/docs/api/configuration/), [Agent Chat API](/docs/api/agent-chat/), [Webhooks](/docs/api/webhooks/)

Bots in Text are dedicated agent identities that operate entirely via the Agent Chat API Web — not RTM, which is for human agents. A bot gets its own token and responds to chats through webhooks rather than by staying connected.

This guide builds a triage bot: it greets every new customer, attempts to match their question against a set of known answers, and transfers to a human if it can't help.

## Prerequisites

* An agent access token with scope `bots--all:rw` and `webhooks--all:rw`. Create one in Text under **Settings → API access → Personal access tokens**.
* A server with a publicly reachable HTTPS endpoint to receive webhooks.
* The group ID of the queue the bot will serve, and the group ID of the human agent queue to hand off to. Find these in Text under **Settings → Chat routing → Groups**.

## Step 1: Create the bot and get its token

Call [`create-bot`](/docs/api/configuration/v3.6/bots/create-bot) with an agent access token. Assign the bot to the group it should serve.

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/configuration/action/create_bot" \
  -H "Authorization: Bearer <AGENT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support Bot",
    "max_chats_count": 10,
    "groups": [{ "id": 1, "priority": "normal" }]
  }'
```

```json theme={null}
{
  "bot_id": "5c9871d5f1079038e289"
}
```

Now get the bot's own access token. **All Agent Chat API calls the bot makes must use this token**, not your agent token.

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/configuration/action/issue_bot_token" \
  -H "Authorization: Bearer <AGENT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "5c9871d5f1079038e289",
    "client_id": "<YOUR_APP_CLIENT_ID>",
    "organization_id": "<YOUR_ORGANIZATION_ID>"
  }'
```

```json theme={null}
{
  "token": {
    "access_token": "dal:test_EOYG-LL-bLbevMtU",
    "token_type": "Bearer"
  }
}
```

Store this `access_token` — your bot uses it for every subsequent call.

## Step 2: Register webhooks

Register two webhooks using the **bot's token** (not your agent token). This scopes them to the bot's activity.

**`incoming_chat`** — fires when a customer starts a new chat in the bot's group:

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/configuration/action/register_webhook" \
  -H "Authorization: Bearer <BOT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "incoming_chat",
    "url": "https://your-server.com/webhooks/text",
    "secret_key": "your-secret-key"
  }'
```

**`incoming_event`** — fires when the customer sends a message:

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/configuration/action/register_webhook" \
  -H "Authorization: Bearer <BOT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "incoming_event",
    "url": "https://your-server.com/webhooks/text",
    "secret_key": "your-secret-key"
  }'
```

Both webhooks deliver to the same URL. Your handler branches on `action` to tell them apart.

## Step 3: Handle incoming chats and messages

When `incoming_chat` fires, the payload includes the full chat object with the `chat_id` you need for all subsequent calls:

```json theme={null}
{
  "action": "incoming_chat",
  "payload": {
    "chat": {
      "id": "PJ0MRSHTDG",
      "users": [
        {
          "id": "b7eff798-f8df-4364-8059-649c35c9ed0c",
          "type": "customer",
          "name": "John Doe"
        }
      ],
      "thread": {
        "id": "K600PKZON8",
        "events": []
      }
    }
  }
}
```

When `incoming_event` fires for a customer message:

```json theme={null}
{
  "action": "incoming_event",
  "payload": {
    "chat_id": "PJ0MRSHTDG",
    "event": {
      "id": "Q0Y2U85XPE3",
      "type": "message",
      "text": "I need to return something",
      "author_id": "b7eff798-f8df-4364-8059-649c35c9ed0c"
    }
  }
}
```

**Sending a message** uses [`send-event`](/docs/api/agent-chat/v3.6/events/send-event) with the bot token:

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/agent/action/send_event" \
  -H "Authorization: Bearer <BOT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": "PJ0MRSHTDG",
    "event": {
      "type": "message",
      "text": "Hi! I can help with orders and returns. What do you need?",
      "visibility": "all"
    }
  }'
```

**Transferring to a human** uses [`transfer-chat`](/docs/api/agent-chat/v3.6/chat-access/transfer-chat). The full message history is preserved — the agent sees everything the bot exchanged:

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/agent/action/transfer_chat" \
  -H "Authorization: Bearer <BOT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "PJ0MRSHTDG",
    "target": {
      "type": "group",
      "ids": [2]
    }
  }'
```

## Step 4: Put it together

Here is a complete webhook handler for the triage pattern — greet, attempt to match, transfer if needed:

```js theme={null}
const ANSWERS = {
  hours: "We're available Monday–Friday, 9am–6pm CET.",
  return: "You can start a return at example.com/returns. Need anything else?",
  shipping: "Standard shipping takes 3–5 business days.",
};

function matchAnswer(text) {
  const lower = text.toLowerCase();
  return Object.entries(ANSWERS).find(([keyword]) =>
    lower.includes(keyword),
  )?.[1];
}

async function botRequest(path, body) {
  const res = await fetch(
    `https://api.livechat.com/v3.6/agent/action/${path}`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.BOT_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    },
  );
  return res.json();
}

async function sendMessage(chatId, text) {
  return botRequest("send_event", {
    chat_id: chatId,
    event: { type: "message", text, visibility: "all" },
  });
}

async function transferToHuman(chatId) {
  return botRequest("transfer_chat", {
    id: chatId,
    target: { type: "group", ids: [Number(process.env.HUMAN_GROUP_ID)] },
  });
}

app.post("/webhooks/text", async (req, res) => {
  const { action, payload } = req.body;

  if (action === "incoming_chat") {
    const chatId = payload.chat.id;
    const name =
      payload.chat.users.find((u) => u.type === "customer")?.name ?? "there";
    await sendMessage(
      chatId,
      `Hi ${name}! I can help with orders, returns, and shipping. What do you need?`,
    );
  }

  if (action === "incoming_event") {
    const { chat_id: chatId, event } = payload;
    if (event.type !== "message") return res.sendStatus(200);

    const answer = matchAnswer(event.text);
    if (answer) {
      await sendMessage(chatId, answer);
    } else {
      await sendMessage(
        chatId,
        "Let me connect you with a team member who can help.",
      );
      await transferToHuman(chatId);
    }
  }

  res.sendStatus(200);
});
```

## Going further

**AI responses**: replace `matchAnswer` with a call to a language model to handle a much wider range of questions before falling back to a human.

**Collecting structured info**: before transferring, have the bot ask for an order number or account email, then use [`update-chat-properties`](/docs/api/agent-chat/v3.6/properties/update-chat-properties) to store the values on the chat object. The agent who picks up the conversation sees them immediately in the customer details panel.

**Typing indicators**: call [`send-typing-indicator`](/docs/api/agent-chat/v3.6/other/send-typing-indicator) before sending a message to make the bot feel more natural.
