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):
# 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/getMeEvery response is a JSON envelope:
{ "ok": true, "result": … }
{ "ok": false, "error": "reason" }A quick check that the bot is alive:
curl -H "Authorization: Bearer kbot_0a1b…" \
https://api.koto.run/getMe
# {"ok":true,"result":{"id":"bot-…","username":"mybot","is_bot":true}}Send a message:
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:
#!/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
doneMethods
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
| Method | HTTP | Parameters | Result |
|---|---|---|---|
getMe | GET | — | { id, username, is_bot } |
getUpdates | GET | ?offset= &timeout= &limit= | array of Update |
getChat | GET | ?chat_id= | { id, type, members } |
Sending and actions
| Method | Body | Notes |
|---|---|---|
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)
| Method | Body | Notes |
|---|---|---|
setMyCommands | { commands: [{ command, description }] } | public list of commands |
getMyCommands | — | current list |
deleteMyCommands | — | clear |
Webhooks
| Method | Body | Notes |
|---|---|---|
setWebhook | { url, secret_token? } | the gateway POSTs updates to url; secret_token is returned as X-Bot-Webhook-Secret |
deleteWebhook | — | back 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_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_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.
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:
PUT /v1/bots/<token>/webapp {"url": "https://example.com/app"}The KotoWebApp bridge
<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>| Member | What 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 |
themeParams | host 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 →
429when exceeded. - • Media — inline base64: send bytes directly in
photo/document, with no two-step upload and nofile_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
| Feature | Status |
|---|---|
| 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.