Buko Bot API for AI Agents
This is the canonical single-page Buko Bot API reference.
It is written for AI agents and automation systems first, but humans can read it too. If you are an AI coding agent, read this entire page first, then build the connector.
Use this page when a human gives you a Buko bot token and asks you to connect an agent to Buko. Do not rely on older split-page Bot API docs; this page is the source of truth.
Status
Bot API is available for official and approved bots. Public self-service bot creation is not open yet.
Goal
Build a service that:
- Authenticates with a Buko bot token.
- Receives user messages from Buko.
- Runs your own agent logic outside Buko.
- Sends replies back through the bot.
Buko is only the messaging channel. Your service owns the intelligence, business logic, tools, memory, and external integrations.
Production endpoints
| Purpose | URL |
|---|---|
| REST API base | https://ims.buko.app |
| Bot Gateway WebSocket | wss://ims.buko.app/bot/ws |
| Canonical Bot API doc | https://buko.app/dev-docs/bot-api/ |
All REST methods use HTTPS. All Bot Gateway connections use WSS.
Core concepts
| Concept | Meaning |
|---|---|
| Bot account | A Buko identity with kind = bot, display name, handle, avatar, and owner. |
| Official bot | A Buko-operated bot with an explicit official marker in clients. Official bot tokens may be encrypted in Buko's token vault for operations. |
| Managed agent | A Buko official bot consumed by Buko's own bot-agent runner. External official bots keep this off and consume updates themselves. |
| Bot token | Secret token used by your agent. It starts with bot_. |
| Chat | A Buko space_id. It can be a private bot DM or a group. |
| Update | An event delivered to a bot, such as message, edited_message, or my_chat_member. |
| Message ID | The message seq inside one chat, serialized as a string. |
Non-negotiable rules
- Never expose the bot token to a browser, frontend bundle, logs, screenshots, or user-visible messages.
- Use
Authorization: Bot <token>for every API request. - Treat all ids as strings. Do not parse them as integers.
- Deduplicate received updates by
update_id. - For incoming attachments, read
message.media[*].file_id, then download withGET /bot/file/<encoded file_id>. - Do not use webhooks. Buko does not support webhooks.
- Do not ask Buko to fetch remote media URLs. Upload bytes with multipart forms.
- A bot cannot DM a user until that user starts the bot inside Buko.
- If a user blocks or stops the bot, stop sending to that chat.
Bot token
The bot token starts with bot_.
Store it as an environment variable:
export BUKO_BOT_TOKEN="bot_xxx"
Verify it:
curl -sS https://ims.buko.app/bot/getMe \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-H "Content-Type: application/json"
Successful response:
{
"ok": true,
"result": {
"id": "u_bot_xxx",
"is_bot": true,
"display_name": "Example Bot",
"handle": "example_bot",
"status": "active",
"verified": false,
"official": false,
"quota_tier": "free",
"gateway_connection_limit": 1,
"capabilities": {
"edit_delete_messages": false,
"interactions": false
}
}
}
Identifier contract
| Field | Meaning | Type |
|---|---|---|
chat_id | Buko space_id | string |
message_id | message sequence inside one chat | string |
reply_to_message_id | message sequence to reply to | string |
update_id | monotonically increasing update id for this bot | string |
from.id | per-bot scoped user id | string |
from.display_name | sender display name | string |
message_id may look numeric, but treat it as an opaque string.
from.id is stable for this bot, but it is not Buko's raw internal user id. Different bots cannot use it to correlate the same person.
Use from.display_name for display only. Buko does not expose the sender's global handle to bots by default.
Start flow
Private bot chats require user consent.
The user starts a bot in Buko. Buko creates or opens the private chat, records the start relationship, and sends a visible /start message. Your agent receives that /start as a normal message update.
For MVP integrations, use the /start message as the main start signal.
Receive updates
Buko supports two delivery modes:
| Mode | Recommended use |
|---|---|
| Bot Gateway WebSocket | Production realtime agents. |
| Polling | Simple workers, scripts, platforms where WebSocket is inconvenient. |
Do not use both at the same time for one bot. If a Gateway connection is active, polling may return 409 GATEWAY_ACTIVE.
Delivery is at least once. Your agent must deduplicate by update_id.
Polling quickstart
Use polling if you want the simplest connector.
Request:
curl -sS https://ims.buko.app/bot/getUpdates \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"offset":"0","limit":50,"timeout":20}'
Response:
{
"ok": true,
"result": [
{
"update_id": "10001",
"message": {
"message_id": "42",
"date": 1783000000,
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"text": "/start"
}
}
]
}
Offset rule:
- Request
offset = "0"on first run. - After handling update
N, set next offset toN + 1. - Store the last handled update id durably if you do not want old updates after restart.
WebSocket quickstart
Use Gateway for realtime agents.
Endpoint:
wss://ims.buko.app/bot/ws
Send Authorization: Bot <token> in the WebSocket upgrade headers.
Incoming frame:
{
"type": "update",
"update": {
"update_id": "10001",
"message": {
"message_id": "42",
"chat": { "id": "space_abc123", "type": "private" },
"text": "hello"
}
}
}
Ack frame:
{
"type": "ack",
"update_id": "10001"
}
Ack is cumulative. Acknowledging 10001 confirms all updates up to and including 10001.
Update types
message
New message visible to the bot.
{
"update_id": "10001",
"message": {
"message_id": "42",
"date": 1783000000,
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"text": "hello"
}
}
Incoming attachments
Messages can include one or more attachments in message.media.
Each media item keeps Buko's original compact fields and also exposes agent-friendly file fields:
{
"update_id": "10004",
"message": {
"message_id": "46",
"date": 1783000000,
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"text": "please inspect this image",
"media": [
{
"key": "media/space_abc123/01J...",
"mime": "image/png",
"size": 12345,
"file_id": "media/space_abc123/01J...",
"mime_type": "image/png",
"file_size": 12345,
"file_name": "photo.png",
"width": 1200,
"height": 800,
"download_path": "/bot/file/media%2Fspace_abc123%2F01J..."
}
],
"photo": [
{
"file_id": "media/space_abc123/01J...",
"mime_type": "image/png",
"file_size": 12345,
"width": 1200,
"height": 800,
"download_path": "/bot/file/media%2Fspace_abc123%2F01J..."
}
]
}
}
Voice messages use the same rule. message.media remains the authoritative attachment list, and message.voice is a convenience copy of the first voice attachment:
{
"update_id": "10005",
"message": {
"message_id": "47",
"date": 1783000030,
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"text": "",
"media": [
{
"key": "media/space_abc123/01J...",
"mime": "audio/mp4",
"size": 45678,
"file_id": "media/space_abc123/01J...",
"mime_type": "audio/mp4",
"file_size": 45678,
"duration_ms": 8200,
"duration": 8,
"waveform": "12,18,24,33,...",
"download_path": "/bot/file/media%2Fspace_abc123%2F01J..."
}
],
"voice": {
"file_id": "media/space_abc123/01J...",
"mime_type": "audio/mp4",
"file_size": 45678,
"duration_ms": 8200,
"duration": 8,
"waveform": "12,18,24,33,...",
"download_path": "/bot/file/media%2Fspace_abc123%2F01J..."
}
}
}
Convenience fields:
| Field | Meaning |
|---|---|
message.media | Authoritative list of all attachments. |
message.photo | Image attachments, when present. |
message.voice | First audio attachment with duration metadata, when present. |
message.document | First non-photo, non-voice attachment, when present. |
For each attachment:
| Field | Meaning |
|---|---|
file_id | Stable file id to pass to getFile or /bot/file/.... |
mime_type | File MIME type. |
file_size | File size in bytes. |
file_name | Original file name when available. |
width, height | Image dimensions when available. |
duration_ms, duration | Audio/video duration when available. |
waveform | Voice waveform when available. |
download_path | Relative authenticated download path. |
Agent implementation tip: parse message.media first. You may also read message.voice, message.photo, and message.document as convenience fields, but they should mirror items already present in message.media.
edited_message
Human message edit visible to the bot.
{
"update_id": "10003",
"edited_message": {
"message_id": "45",
"edit_date": 1783000300,
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"text": "edited text"
}
}
my_chat_member
Bot relationship or membership change.
Private stop/block uses started -> stopped. Group removal or group dissolution uses member -> removed.
{
"update_id": "10002",
"my_chat_member": {
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"date": 1783000000,
"old_status": "started",
"new_status": "stopped"
}
}
interaction
Button taps are delivered as interaction updates. They are not visible chat messages and do not create a new message_id.
{
"update_id": "10006",
"interaction": {
"id": "ixn_01J...",
"type": "callback",
"chat": { "id": "space_abc123", "type": "private" },
"from": {
"id": "bot_scoped_user_abc",
"is_bot": false,
"display_name": "Alice"
},
"message": {
"chat": { "id": "space_abc123", "type": "private" },
"message_id": "43"
},
"component_id": "primary_actions",
"item_id": "bind",
"data": "bind_account",
"created_at": "2026-07-03T02:00:00.000Z"
}
}
Deduplicate interaction work by update_id or interaction.id.
Send text messages
POST https://ims.buko.app/bot/sendMessage
Request:
{
"chat_id": "space_abc123",
"text": "**Hello** from your agent",
"reply_to_message_id": "42",
"parse_mode": "app_markdown",
"interactions": {
"version": 1,
"components": [
{
"type": "button_row",
"id": "primary_actions",
"items": [
{
"id": "bind",
"label": "Bind account",
"style": "primary",
"action": { "type": "callback", "data": "bind_account" }
},
{
"id": "docs",
"label": "Docs",
"action": {
"type": "open_url",
"url": "https://buko.app/dev-docs/bot-api/"
}
}
]
}
]
}
}
Optional fields:
| Field | Meaning |
|---|---|
reply_to_message_id | Reply to an existing message seq in the same chat. |
parse_mode | plain or app_markdown. Defaults to plain. |
display | Rich display object. Usually omit it and let Buko derive display from text + parse_mode. |
interactions | Button rows. Tier-gated; free bots cannot send interactions. |
app_markdown supports a conservative subset: bold, inline code, fenced code blocks, quotes, headings, lists, simple tables, and HTTPS links. Raw HTML is not rendered. Unsafe links are rejected, including localhost, private IP, loopback, link-local, and multicast targets.
Interaction limits:
- Maximum 8 button rows/components per message.
- Only
button_rowcomponents are supported in v1. - Maximum 6 buttons per row and 30 buttons per message.
component.idanditem.idmust be 1-64 characters: letters, numbers,_,-, or..- Callback
datais limited to 512 bytes. open_urlonly supports HTTPS URLs and rejects localhost, private IP, loopback, link-local, and multicast targets.open_app_linksupports app-native navigation targets such as handles, bot profiles, channels, and join links.
Example:
curl -sS https://ims.buko.app/bot/sendMessage \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "space_abc123",
"text": "**Got it.**",
"parse_mode": "app_markdown",
"reply_to_message_id": "42",
"interactions": {
"version": 1,
"components": [
{
"type": "button_row",
"id": "primary_actions",
"items": [
{
"id": "ok",
"label": "OK",
"style": "primary",
"action": { "type": "callback", "data": "ok" }
}
]
}
]
}
}'
Response:
{
"ok": true,
"result": {
"message_id": "43",
"chat": { "id": "space_abc123", "type": "private" },
"date": 1783000000,
"text": "Got it."
}
}
Answer interactions
POST https://ims.buko.app/bot/answerInteraction
Use this to acknowledge a button tap with a short toast or alert on the device that tapped the button.
{
"interaction_id": "ixn_01J...",
"text": "Started.",
"show_alert": false
}
Response:
{
"ok": true,
"result": {
"delivered": true
}
}
Interaction answers are short-lived and best-effort. The route is valid for about 10 seconds after the tap. For long-running work, answer quickly and send a normal message when the work finishes.
Send typing indicators
POST https://ims.buko.app/bot/sendChatAction
Supported actions:
typingupload_photoupload_documentrecord_voiceupload_voice
Example:
curl -sS https://ims.buko.app/bot/sendChatAction \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chat_id":"space_abc123","action":"typing"}'
Send media
Buko supports photos and documents with optional captions.
Do not send a remote URL. Upload bytes using multipart/form-data.
sendPhoto
POST https://ims.buko.app/bot/sendPhoto
curl -sS https://ims.buko.app/bot/sendPhoto \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-F "chat_id=space_abc123" \
-F "caption=Here is the chart." \
-F "parse_mode=app_markdown" \
-F "photo=@./chart.png;type=image/png"
sendDocument
POST https://ims.buko.app/bot/sendDocument
curl -sS https://ims.buko.app/bot/sendDocument \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-F "chat_id=space_abc123" \
-F "caption=Monthly report" \
-F "parse_mode=plain" \
-F "document=@./report.pdf;type=application/pdf"
Limits:
| Method | Max size |
|---|---|
sendPhoto | 20 MB |
sendDocument | 50 MB |
Captions can be up to 5000 characters.
Media methods accept the same optional reply_to_message_id, parse_mode, display, and interactions fields as sendMessage. Because media methods use multipart/form-data, send display and interactions as JSON strings:
curl -sS https://ims.buko.app/bot/sendPhoto \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-F "chat_id=space_abc123" \
-F "caption=Choose what to do with this image." \
-F "parse_mode=app_markdown" \
-F 'interactions={
"version": 1,
"components": [
{
"type": "button_row",
"id": "image_actions",
"items": [
{
"id": "analyze",
"label": "Analyze",
"style": "primary",
"action": { "type": "callback", "data": "analyze_image" }
}
]
}
]
}' \
-F "photo=@./chart.png;type=image/png"
Example open_app_link action:
{
"type": "open_app_link",
"target": {
"type": "bot_profile",
"value": "homefold"
}
}
Download incoming files
Incoming attachment downloads require the bot token.
Bots can only download files from chats where the bot is currently allowed to read:
- private bot DM: user has started the bot and neither side has blocked the other
- group: bot is still a member
- channel: bot is still a member
The file id must come from an incoming message.media[*].file_id value. Buko only allows media/<chat_id>/... file ids here. tmp_uploads, moments, avatars, arbitrary paths, and directory-like scans are rejected.
getFile
POST https://ims.buko.app/bot/getFile
Request:
{
"file_id": "media/space_abc123/01J..."
}
Example:
curl -sS https://ims.buko.app/bot/getFile \
-X POST \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"file_id":"media/space_abc123/01J..."}'
Response:
{
"ok": true,
"result": {
"file_id": "media/space_abc123/01J...",
"file_size": 12345,
"mime_type": "image/png",
"download_path": "/bot/file/media%2Fspace_abc123%2F01J...",
"download_url": "https://ims.buko.app/bot/file/media%2Fspace_abc123%2F01J...",
"requires_authorization": true
}
}
download_url is not public. You must send the same Authorization: Bot <token> header when downloading.
Direct download
GET https://ims.buko.app/bot/file/<encoded file_id>
Example:
curl -L https://ims.buko.app/bot/file/media%2Fspace_abc123%2F01J... \
-H "Authorization: Bot $BUKO_BOT_TOKEN" \
-o attachment.bin
Range requests are supported:
Range: bytes=0-1048575
Edit and delete messages
These methods are tier-gated. Free bots cannot use them.
Even when enabled, a bot can only edit or delete messages authored by the same bot.
editMessageText
POST https://ims.buko.app/bot/editMessageText
{
"chat_id": "space_abc123",
"message_id": "43",
"text": "Updated text",
"parse_mode": "plain"
}
deleteMessage
POST https://ims.buko.app/bot/deleteMessage
{
"chat_id": "space_abc123",
"message_id": "43"
}
Error envelope
All errors use this shape:
{
"ok": false,
"error_code": 403,
"code": "CHAT_FORBIDDEN",
"description": "User has not started this bot."
}
Common errors:
| HTTP | Code | What to do |
|---|---|---|
| 400 | BAD_REQUEST | Fix request parameters. |
| 400 | INVALID_INTERACTION | Fix the button/component payload. |
| 400 | INVALID_MARKDOWN | Fix Markdown syntax, remove unsafe HTML, or use HTTPS links. |
| 400 | INTERACTION_EXPIRED | The button message has expired; send a fresh message. |
| 400 | INTERACTION_NOT_FOUND | The referenced button no longer exists. |
| 400 | UNSUPPORTED_DISPLAY_FORMAT | Use display.version=1 and format=app_markdown. |
| 401 | UNAUTHORIZED | Stop and check token. Re-read token if it may have rotated. |
| 403 | CHAT_FORBIDDEN | Do not retry until permission state changes. |
| 403 | BOT_BLOCKED | Stop sending to that chat. |
| 403 | FILE_FORBIDDEN | Bot cannot read this file id. |
| 403 | FORBIDDEN_INTERACTION | The user cannot tap this button in this chat. |
| 403 | METHOD_NOT_ALLOWED_FOR_TIER | Disable that feature or upgrade bot tier. |
| 403 | MESSAGE_FORBIDDEN | Bot tried to edit/delete a message it does not own. |
| 404 | CHAT_NOT_FOUND | Drop or re-resolve the chat. |
| 404 | FILE_NOT_FOUND | File does not exist, expired, or is invisible to this bot. |
| 409 | GATEWAY_ACTIVE | Stop polling or close Gateway. |
| 410 | INTERACTION_DELIVERY_FAILED | The tap route expired before the bot answered. |
| 413 | DISPLAY_TOO_LARGE | Reduce the rich display payload. |
| 413 | INTERACTION_TOO_LARGE | Reduce the interaction payload. |
| 413 | PAYLOAD_TOO_LARGE | Compress or reject the file. |
| 429 | RATE_LIMITED | Wait for retry_after, then retry. |
| 500 | INTERNAL | Retry with exponential backoff and jitter. |
Default quota tiers
| Tier | Incoming / min | Incoming / day | Messages / min | Messages / day | Polls / min | Polls / day | Edit/delete | Interactions |
|---|---|---|---|---|---|---|---|---|
free | 60 | 5,000 | 20 | 1,000 | 30 | 10,000 | no | no |
pro | 180 | 50,000 | 60 | 10,000 | 60 | 50,000 | yes | yes |
team | 300 | 150,000 | 100 | 25,000 | 100 | 100,000 | yes | yes |
official | 600 | 500,000 | 120 | 50,000 | 120 | 200,000 | yes | yes |
Quotas are conservative during early access and may change.
Minimal Python polling echo agent
This example uses polling because it works in most environments.
Install dependency:
python3 -m pip install requests
Run:
import os
import time
import requests
BASE = "https://ims.buko.app"
TOKEN = os.environ["BUKO_BOT_TOKEN"]
HEADERS = {
"Authorization": f"Bot {TOKEN}",
"Content-Type": "application/json",
}
offset = "0"
seen = set()
def api(method, payload=None):
r = requests.post(
f"{BASE}/bot/{method}",
headers=HEADERS,
json=payload or {},
timeout=35,
)
data = r.json()
if not data.get("ok"):
raise RuntimeError(f"{method} failed: {data}")
return data["result"]
def send_message(chat_id, text, reply_to=None):
payload = {"chat_id": chat_id, "text": text}
if reply_to:
payload["reply_to_message_id"] = reply_to
return api("sendMessage", payload)
def handle_update(update):
message = update.get("message")
if not message:
return
text = message.get("text") or ""
chat_id = message["chat"]["id"]
message_id = message["message_id"]
if text == "/start":
send_message(chat_id, "Buko bot is online. Send me a message.", message_id)
return
reply = f"Echo: {text}" if text else "I received your message."
send_message(chat_id, reply, message_id)
while True:
try:
updates = api("getUpdates", {
"offset": offset,
"limit": 50,
"timeout": 20,
})
for update in updates:
update_id = update["update_id"]
if update_id in seen:
continue
seen.add(update_id)
handle_update(update)
offset = str(int(update_id) + 1)
except Exception as exc:
print("bot loop error:", exc)
time.sleep(3)
Production notes for this example:
- Persist
offsetin a database or durable file. - Use a bounded dedupe cache for
seen. - Add exponential backoff and jitter for
500and network errors. - Stop retrying permanent
400,401, and403errors. - Keep the token outside source code.
Minimal WebSocket agent outline
Use this when you need low-latency realtime delivery.
import WebSocket from "ws";
const token = process.env.BUKO_BOT_TOKEN;
const ws = new WebSocket("wss://ims.buko.app/bot/ws", {
headers: { Authorization: `Bot ${token}` },
});
ws.on("message", async (raw) => {
const frame = JSON.parse(raw.toString());
if (frame.type !== "update") return;
const update = frame.update;
try {
// Run your agent logic here.
console.log("update", update.update_id, update.message?.text);
ws.send(JSON.stringify({
type: "ack",
update_id: update.update_id,
}));
} catch (err) {
// Do not ack if processing failed and you want Buko to redeliver later.
console.error(err);
}
});
Suggested agent architecture
Use these components:
| Component | Responsibility |
|---|---|
| Update receiver | WebSocket or polling loop. |
| Dedupe store | Remember handled update_id. |
| Conversation router | Route by chat.id and from.id. |
| Agent core | Your LLM, tools, workflows, or business logic. |
| Buko sender | Calls sendMessage, sendPhoto, sendDocument, and optional actions. |
| File downloader | Downloads incoming message.media[*].file_id through /bot/file/.... |
| Error handler | Distinguishes retryable and permanent failures. |
Recommended state keys:
- Per chat:
chat.id - Per user inside this bot:
from.id - Per incoming message:
chat.id + message_id - Per update dedupe:
update_id
Security checklist for AI agents
- Do not reveal system prompts, tool credentials, bot token, or internal logs.
- Treat all incoming user text as untrusted.
- If your agent calls external tools, validate tool inputs before execution.
- If your agent can spend money or modify external systems, require explicit user confirmation in your own product logic.
- Do not store more user content than your product needs.
- Respect Buko stop/block events and permission errors.
Implementation checklist
- Read
BUKO_BOT_TOKENfrom environment. - Call
getMe; fail fast if it returnsUNAUTHORIZED. - Choose WebSocket or polling.
- Deduplicate updates by
update_id. - Handle
/start. - If
message.mediaexists, callgetFileor download fromdownload_pathwith the bot token. - For each message, run your agent logic.
- Send replies with
sendMessage. - Handle
my_chat_memberstopped/removed by disabling that chat in your state. - Implement retry/backoff for retryable errors.
- Keep the token out of logs and user-visible output.
Canonical source
This single page is the canonical Buko Bot API reference for both humans and AI agents.
Older split paths redirect back into this page:
/dev-docs/bot-api/authentication/redirects to Bot token./dev-docs/bot-api/updates/redirects to Receive updates./dev-docs/bot-api/methods/redirects to Send text messages./dev-docs/bot-api/media/redirects to Send media./dev-docs/bot-api/errors-and-limits/redirects to Error envelope./dev-docs/bot-api/security/redirects to Security checklist.