Documentation · Bot API

Koto bots in any language

A simple HTTP API. You send plain HTTP + JSON; the Koto gateway holds the bot's E2E identity and inbox, decrypts incoming messages and turns your calls into encrypted Koto operations. You never touch the cryptography.

Overview

The API follows a familiar pattern: the method is the path, the token rides in an Authorization header, and the response is a JSON envelope { ok, result }. No SDK is required — you only need to make HTTP requests. The call format is compatible with existing client libraries for the Telegram Bot API*, so often it's enough to point their base URL at the Koto gateway.

The flow is simple: create a bot in @BotKoto and get a token → call methods with that token in an Authorization header → receive updates through long-poll getUpdates or a webhook.

* Telegram is a trademark of Telegram Messenger LLP. Koto is developed independently, is not affiliated with or endorsed by Telegram; the name is used solely to describe compatibility.

1. Create a bot

Open @BotKoto in any Koto client, send /newbot, choose a name and @username. You'll receive a token like kbot_0a1b2c…. Keep it secret — it controls the bot.

2. Call the API

The method is the whole path; the token rides in an Authorization: Bearer header (never the URL, so it can't leak into logs):

endpoint
# the method is the whole path; the token rides in a header
{GATEWAY}/{method}
Authorization: Bearer {TOKEN}

# example
curl -H "Authorization: Bearer kbot_0a1b…" \
  https://api.koto.run/getMe

Every response is a JSON envelope:

envelope
{ "ok": true,  "result":  }
{ "ok": false, "error": "reason" }

A quick check that the bot is alive:

getMe
curl -H "Authorization: Bearer kbot_0a1b…" \
  https://api.koto.run/getMe
# {"ok":true,"result":{"id":"bot-…","username":"mybot","is_bot":true}}

Send a message:

sendMessage
curl -X POST https://api.koto.run/sendMessage \
  -H "Authorization: Bearer kbot_…" \
  -H 'content-type: application/json' \
  -d '{"chat_id":"KOTO-…","text":"Hello from Koto!"}'

3. Receive updates

Two mutually exclusive ways:

Long-poll getUpdates — pull updates and acknowledge with a growing offset. offset = last update_id + 1 acknowledges everything below it. getUpdates holds the connection for up to timeout seconds (25 by default).

Webhook setWebhook — the gateway itself POST of each update to your URL. While a webhook is set, getUpdates returns 409.

Example: echo bot

A full bot in pure bash — only HTTP + JSON, no Koto SDK and no crypto. Long-poll getUpdates and reply via sendMessage:

echo.sh
#!/usr/bin/env bash
# A full bot over plain HTTP — no SDK, no crypto. The token rides in an
# Authorization: Bearer header, never the URL.
# Run:  BOT_TOKEN=kbot_xxx ./echo.sh
API="http://127.0.0.1:8090"
AUTH=(-H "Authorization: Bearer $BOT_TOKEN")

offset=0
while true; do
  resp=$(curl -s "${AUTH[@]}" "$API/getUpdates?offset=$offset&timeout=20")
  for upd in $(echo "$resp" | jq -c '.result[]?'); do
    offset=$(( $(jq '.update_id' <<<"$upd") + 1 ))
    chat=$(jq -r '.message.chat.id // ""' <<<"$upd")
    text=$(jq -r '.message.text // ""' <<<"$upd")
    [ -n "$chat" ] && [ -n "$text" ] && \
      curl -s -X POST "${AUTH[@]}" "$API/sendMessage" -H 'content-type: application/json' \
        -d "$(jq -nc --arg c "$chat" --arg t "Echo: $text" '{chat_id:$c,text:$t}')"
  done
done

Methods

chat_id is what arrived in message.chat.id of the update. Message identifiers are opaque strings. A successful send returns { "message_id": "…" }, a void operation returns true.

Reading

MethodHTTPParametersResult
getMeGET{ id, username, is_bot }
getUpdatesGET?offset= &timeout= &limit=array of Update
getChatGET?chat_id={ id, type, members }

Sending and actions

MethodBodyNotes
sendMessage{ chat_id, text, reply_markup?, reply_to_message_id? }text, optional inline keyboard and quoted reply
sendPhoto{ chat_id, photo, mime, w, h, caption? }photo — base64 of the image bytes
sendDocument{ chat_id, document, file_name, mime, size }document — base64 of the file
editMessageText{ chat_id, message_id, text }edit the text
editMessageReplyMarkup{ chat_id, message_id, reply_markup }an empty markup removes the keyboard
deleteMessage{ chat_id, message_id }delete
forwardMessage{ chat_id, text, from? }from — the "forwarded from" label
sendPoll{ chat_id, question, options[], is_anonymous? }poll
pinChatMessage{ chat_id, message_id }pin
unpinChatMessage{ chat_id }unpin
setMessageReaction{ chat_id, message_id, emoji, add? }add defaults to true
sendChatAction{ chat_id, action? }shows "typing…"
answerCallbackQuery{ callback_query_id }no-op ack (Koto has no spinner on the button)

Command menu (the "/" list)

MethodBodyNotes
setMyCommands{ commands: [{ command, description }] }public list of commands
getMyCommandscurrent list
deleteMyCommandsclear

Webhooks

MethodBodyNotes
setWebhook{ url, secret_token? }the gateway POSTs updates to url; secret_token is returned as X-Bot-Webhook-Secret
deleteWebhookback to getUpdates
getWebhookInfo{ url, pending_update_count, … }

The Update object

getUpdates (and the webhook POST body) return an Update. Exactly one payload field is filled per update:

Update
{
  "update_id": 42,
  "message":          {  },   // new incoming message
  "edited_message":   {  },   // a message's text was edited
  "callback_query":   {  },   // inline button press
  "poll_answer":      {  },   // poll vote
  "message_reaction": {  }    // reaction toggled
}

Message

Message
{
  "message_id": "…",
  "date": 1733836800,
  "chat": { "id": "…" },
  "from": { "id": "KOTO-…" },
  "text": "hello",
  "caption": "…",                                  // for a photo/document
  "photo":    { "data": "<base64>", "mime": "image/jpeg", "w": 1280, "h": 720 },
  "document": { "data": "<base64>", "file_name": "…", "mime": "…", "size": 1234 },
  "reply_to_message": {  },                       // if this is a quoted reply
  "forward_from": "Alice",                         // if forwarded
  "reply_markup": [[  ]]                          // attached keyboard
}

callback_query: { id, from, chat, message_id, data } — pass chat.id into sendMessage to respond to the tap.

Inline buttons

reply_markup is rows of buttons. Each button is { text, data }:

reply_markup
{
  "chat_id": "KOTO-…",
  "text": "Choose an option",
  "reply_markup": [
    [ { "text": "Yes", "data": "y" }, { "text": "No", "data": "n" } ]
  ]
}

A tap arrives as a callback_query update with the data field of the tapped button. You can respond to it by calling answerCallbackQuery (in Koto a harmless ack) and sending a message to the chat.id from the callback.

Mini apps

A mini app is a web page the bot opens inside a Koto chat. The user taps the app button (▦) next to the input field, the page loads in a sandbox and can talk to the bot. The URL must be HTTPS.

Set the mini app URL

Via a Koto client (bot → Mini App → paste the URL) or via the registry with the bot's management token:

registry
PUT /v1/bots/<token>/webapp     {"url": "https://example.com/app"}

The KotoWebApp bridge

index.html
<script src="koto-webapp.js"></script>
<script>
  KotoWebApp.ready();                       // theme + platform from the host
  KotoWebApp.MainButton.setText("Done").show();
  KotoWebApp.MainButton.onClick(function () {
    KotoWebApp.sendData(JSON.stringify({ ok: true }));  // → sent to the bot
  });
</script>
MemberWhat it does
ready()tell the host the page has loaded; fills in themeParams/platform
sendData(string)send a string to the bot, then the app closes
close() / expand()close / expand the sheet to full height
openLink(url)open a URL in the user's browser
themeParamshost colors (also as CSS variables --koto-*)
MainButton.setText .show .hide .enable .disable .onClick
onEvent(name, cb)events: ready, themeChanged, mainButtonClicked

sendData(s) delivers s to the bot as a normal incoming message (the bot sees it via getUpdates/webhook). Send compact JSON and parse it in the bot.

Limits and specifics

  • Rate limit: per bot, 30 requests/sec by default → 429 when exceeded.
  • Media — inline base64: send bytes directly in photo / document, with no two-step upload and no file_id.
  • Identifiers are Koto IDs (KOTO-… for users, bot-… for bots), not numeric.
  • End-to-end: messages are encrypted on the wire, the blind relay never sees plaintext. The gateway decrypts on the bot's behalf — so the bot operator (and the gateway hosting it) see what is written to the bot. Chats with a bot are not private from the bot itself. User↔user conversations are not affected by this.

Feature status

FeatureStatus
Text, photo, document, polls✅ implemented
Edit / delete / pin / reactions / forward✅ implemented
Inline keyboards + taps (callback_query)✅ implemented
Command menu (setMyCommands)✅ implemented
getUpdates long-poll and webhooks✅ implemented
Quoted replies✅ implemented
Bots in groups/channels (>2 members)⚠️ methods work in a group the bot is already in; adding to a group via the Bot API is not wired up yet
Inline mode (@bot … in any chat)❌ not supported yet
In-bot payments❌ not supported yet

Hosting

Most bots don't need hosting: you work through the managed Koto gateway — your kbot_… token and the public base URL are enough.

Need full control over the keys? The gateway and a single connector (you keep the bot's keys yourself, same API) can be deployed on your own. Deployment options and environment variables are described in the README of the koto-bot — here, in the public API reference, we don't duplicate them.

Download for Windows