FALCON BLACK Русский
Bots for Falcon Black
A bot is a program that answers people in Falcon Black. You create it in a chat with @botnest (Bot Nest), get a key on the website and connect your program to the Bot API: it receives people's messages and replies. The API shape will feel familiar if you have written bots for other platforms: same method and field names.
getUpdates) or a webhook (setWebhook). Mini apps are below. Inline mode comes
next; its calls get a clear refusal.Quick start
- Open a chat with @botnest in Falcon Black and send
/newbot. - Send the bot's name, then a username in Latin letters ending in
bot. - Send
/token— Bot Nest replies with a one-time code (10 minutes). The key itself is never sent in a chat. - Open falconblack.org/bots/key, enter the code and your Falcon Black password — the key
is shown once. Put it into the
FB_BOT_TOKENenvironment variable. - Run an example below and write to your bot.
Address and key
All methods: https://falconblack.org/bot-api/v1/<method>, GET or POST
(JSON, form or multipart/form-data). The key goes only in the header
Authorization: Bearer <key>. A key inside a request address is refused and disabled at once
(query string or a /bot<key>/method path): addresses end up in proxy logs.
In the app, a key is issued the same way — My bots → bot → New key, with your account password — and is also shown
once: we don't store the key itself, only its fingerprint. Revoke is there too.
import zlib from "node:zlib"; // Node 20.15+ / 22
const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
export function keyLooksRight(key) {
const m = /^(\d{10,16}:fb1_[0-9A-Za-z]{43})([0-9A-Za-z]{6})$/.exec(key);
if (!m) return false;
let n = zlib.crc32(m[1]), sum = "";
for (let i = 0; i < 6; i++) { sum = B62[n % 62] + sum; n = Math.floor(n / 62); }
return sum === m[2];
}
Example: echo bot
No libraries, long polling, tested against the Falcon Black server.
echo.mjs (Node.js 20+), echo.py
(Python 3.9+ with requests); full examples: full.mjs,
full.py. Comments in the examples are in Russian.
// Эхо-бот Falcon Black: Node.js 20+, без библиотек, длинный опрос.
// Запуск: FB_BOT_TOKEN='ключ со страницы falconblack.org/bots/key' node echo.mjs
// Ключ — только в переменной окружения и только в заголовке Authorization: Bearer
// (ключ в адресе запроса сервер сразу отключает).
const API = process.env.FB_API || "https://falconblack.org/bot-api/v1";
const HEADERS = { Authorization: `Bearer ${(process.env.FB_BOT_TOKEN || "").trim()}`, "Content-Type": "application/json" };
async function call(method, params = {}) {
const r = await fetch(`${API}/${method}`, { method: "POST", headers: HEADERS, body: JSON.stringify(params) });
let j;
try { j = await r.json(); } catch { throw new Error("ответ не JSON — сбой сети, повторим"); }
if (j.ok) return j.result;
throw Object.assign(new Error(j.description), { code: j.error_code, key: j.error_key, retryAfter: j.parameters?.retry_after });
}
// Кнопки люди пока видят списком с номерами под сообщением: ответ «1» приходит
// программе нажатием (callback_query). Ответ цифрой относится к ПОСЛЕДНЕМУ
// сообщению бота — поэтому кнопка есть под каждым ответом.
const DICE = { inline_keyboard: [[{ text: "Бросить кубик", callback_data: "roll" }]] };
async function handle(u) {
const m = u.message;
if (m?.text?.startsWith("/start")) {
// «/start» приходит первым, когда человек впервые написал боту.
await call("sendMessage", { chat_id: m.chat.id, text: "Здравствуйте! Напишите что-нибудь — я повторю.", reply_markup: DICE });
} else if (m?.text) {
await call("sendMessage", { chat_id: m.chat.id, text: `Вы написали: ${m.text}`, reply_markup: DICE });
} else if (u.callback_query?.data === "roll") {
const n = 1 + Math.floor(Math.random() * 6);
await call("answerCallbackQuery", { callback_query_id: u.callback_query.id });
await call("sendMessage", { chat_id: u.callback_query.message.chat.id, text: `Выпало ${n}`, reply_markup: DICE });
}
}
let offset = 0;
for (;;) {
let updates;
try {
updates = await call("getUpdates", { offset, timeout: 50 });
} catch (e) {
// 401 — ключ (причина — в e.key), 409 — запущена вторая копия: повтор не поможет.
if ([401, 403, 409].includes(e.code)) { console.error(`${e.code} ${e.key}: ${e.message}`); process.exit(1); }
await new Promise((ok) => setTimeout(ok, (e.retryAfter ?? 3) * 1000)); // сбой сети или 429
continue;
}
for (const u of updates) {
offset = u.update_id + 1;
// Ошибка одного события (человек заблокировал бота — 403, предел — 429) не роняет программу.
await handle(u).catch((e) => console.warn(`событие ${u.update_id}: ${e.code ?? ""} ${e.message}`));
}
}
Bot profile
What people see in the bot's profile, in search and in an empty chat: the name (up to 64 characters), the picture, About (up to 120 characters — in the profile and in search) and “What can this bot do” (up to 512 characters — in an empty chat, before Start). Change them in @botnest or in the app: My bots → bot → Info. Names and texts must not make the bot look like Falcon Black.
Commands
A command is a word after “/” that people pick in the bot's menu: up to 100 commands, each 1–32 characters (Latin
letters, digits and “_”, or Cyrillic), with a description up to 256 characters. Set the menu in @botnest, in the app
(My bots → bot → Commands) or from your program with setMyCommands — also per language
(language_code) and for groups (scope).
Methods
getMe, getUpdates (offset, limit 1–100, timeout 0–50 s),
sendMessage, sendPhoto (upload up to 10 MB or file_id), sendDocument
(upload up to 20 MB or file_id), editMessageText, editMessageReplyMarkup,
deleteMessage (own messages under 48 h), answerCallbackQuery, sendChatAction,
setMyCommands / getMyCommands / deleteMyCommands, getChat,
sendVideo (MP4/MOV), sendAudio (MP3, M4A, WAV, FLAC), sendVoice (M4A or MP3 —
no OGG/Opus yet), sendSticker (only from Falcon Black sticker sets: a received sticker's file_id
or getStickerSet), sendLocation (no live location yet), sendContact (delivered as
text), setMessageReaction (one emoji, including a flag or a keycap digit; one reaction per message. In a channel — only the reactions its owner allows, and no more different reactions per post than set: getChat → available_reactions, max_reaction_count), setWebhook / deleteWebhook /
getWebhookInfo, setChatMenuButton / getChatMenuButton, answerWebAppQuery
(mini apps; getMe.has_main_web_app).
Webhook
https only, ports 443, 88 or 8443, a host name (not an IP), no user info; internal addresses and
falconblack.org are refused (bad_webhook). The host is checked at setWebhook and
before every delivery. Each update is a POST with Falcon-Update-Id and, if you set
secret_token, the X-Falcon-Bot-Api-Secret-Token header — the name differs from other
platforms, so check it yourself. 2xx within 10 s means delivered; network failures are retried (5 s … then hourly, for a
day); an update answered with 4xx/5xx five times in a row is dropped. One person's updates go strictly in order;
different people in parallel, up to max_connections (max 10). A new URL gets one update at a time, at most
every 5 s, until it answers 2xx once. 15 minutes without success — falcon_state: "failing" and a message from
@botnest. While a webhook is set, getUpdates returns 409. A new key or revoking the key removes the webhook.
// Node.js 20+: приёмник вебхука. Свой https-сервер или прокси перед ним (порт 443, 88 или 8443).
import http from "node:http";
const SECRET = process.env.FB_WEBHOOK_SECRET; // тот же, что в setWebhook
http.createServer((req, res) => {
// Сверяйте НАШ заголовок сами: готовые библиотеки ищут заголовок с другим именем.
if (req.headers["x-falcon-bot-api-secret-token"] !== SECRET) { res.writeHead(401).end(); return; }
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
const update = JSON.parse(body); // update_id — и в заголовке Falcon-Update-Id
res.writeHead(200).end(); // 2xx за 10 с — доставлено; работу — после ответа
handle(update);
});
}).listen(8080); // за прокси с https
// Один раз, при запуске программы:
// curl -H "Authorization: Bearer $FB_BOT_TOKEN" -d url=https://bot.example.org/hook \
// -d secret_token=$FB_WEBHOOK_SECRET https://falconblack.org/bot-api/v1/setWebhook
Events and buttons
- First message from a person:
my_chat_member(left→member), then/start, then the message itself. Blocking the bot:member→kicked, andsendMessagereturns 403 “bot was blocked by the user”. - New Falcon Black apps draw buttons; in older app versions an
inline_keyboardis shown as a numbered list, and a reply with a number (or the button's text) arrives ascallback_querywithfalcon_via. - Bot link:
https://falconblack.org/bot/<username>; with?start=<parameter>(up to 64 ofA-Z a-z 0-9 _ -) the program gets/start <parameter>when the person taps Start. parse_modeis accepted and the markup is stripped.
What a bot knows about a person
A number unique to your bot, the profile name and the app language. No username, phone or photo. A bot can only write to people who wrote to it first and haven't blocked it. Everything a person writes to a bot goes to the bot's author.
Errors and limits
{"ok": true, "result": {...}}
{"ok": false, "error_code": 403, "description": "Forbidden: bot was blocked by the user",
"error_key": "blocked"}
{"ok": false, "error_code": 429, "description": "Too Many Requests: retry after 3",
"error_key": "too_many", "parameters": {"retry_after": 3}}
HTTP status equals error_code; error_key is a stable machine code; every response has
X-Request-Id. Sending: new bots (first 3 days) 10/s and 300/min, others 30/s and 1200/min; one chat — 1/s,
burst 5; text 4096 UTF-16 units, caption 1024; photo 10 MB, file, video, audio and voice 20 MB, at most 2 uploads at a time per key;
getUpdates 30/s for new bots, 50/s for others. New bots may send links only to their owner.
Limits are explicit: 429 with retry_after.
Groups and channels
A group or channel admin adds the bot — from the bot's profile or by a link. The bot's author can forbid this with
/setjoingroups in @botnest or in My bots; that doesn't remove the bot from where it already is. In a group
the bot is an ordinary member (everyone sees it, older app versions too); in a channel a bot is always an admin.
- The chat number is a negative
chat_id("type": "group"or"channel", withtitle), unique to your bot; it arrives inmy_chat_memberwhen the bot is added and stays the same if the bot is removed and added again. Kicked — 403 “bot was kicked from the group chat”; left — 403 “bot is not a member of the group chat”. - A group member gets a number at your bot (
from.id), but the bot can't message them privately until they write to the bot first (403 “bot can't initiate conversation with a user”). - Groups and channels take only inline buttons: reply keyboards,
remove_keyboardandforce_reply— 400param_unsupported. A new bot (first 3 days, untilfalcon_trust_until) can't send links or URL buttons to groups and channels — 403links_new_bot. Other new keys:bot_kicked,not_member,cant_initiate,not_enough_rights(403);user_not_found,member_list_inaccessible,user_is_admin,private_chat(400). - Privacy mode (on for new bots): the bot gets
/command@your_bot, a plain/commandif it was the last bot to write in the group, replies to its messages and service events — a bare@usernamemention is not delivered. Privacy off: all messages. A bot that is a group admin gets all messages whatever the mode. The mode is fixed when the bot is added: changing it (/setprivacy) affects groups the bot is added to afterwards. Other bots' messages are never delivered. Members see a line when the bot is added (“It sees only commands and replies to its messages” or “It sees all messages in the group, and they go to the bot's author”). - Channels: the bot gets
channel_postandedited_channel_post; it posts withsendMessageand thecan_post_messagesright (on by default); posts are signed by the channel. Own posts can be edited and deleted any time. Example news bot: news.mjs, news.py. - Rights — the
ChatAdministratorRightsnames: groups —can_change_info,can_delete_messages,can_restrict_members,can_invite_users,can_pin_messages,can_promote_members,is_anonymous; channels —can_change_info,can_post_messages,can_edit_messages,can_delete_messages,can_invite_users,can_promote_members. Checked on every call. Methods:getChat,getChatMember,getChatAdministrators,getChatMemberCount,leaveChat,pinChatMessage,unpinChatMessage,unpinAllChatMessages,banChatMember,unbanChatMember,promoteChatMember,setChatTitle,setChatDescription,setChatPhoto,deleteChatPhoto,exportChatInviteLink(the bot's own main link,is_primary: true),createChatInviteLink(withname, up to 32 characters, seen only by admins),editChatInviteLink(changes only the fields you send;expire_date: 0,member_limit: 0andname: ""clear them; the main link can't be edited),revokeChatInviteLink(own links only; up to 30 active extra links per chat),creates_join_request(people join only after approval; not withmember_limit, not in public channels; omitted ineditChatInviteLink— unchanged; links showpending_join_request_count),approveChatJoinRequestanddeclineChatJoinRequest(withcan_invite_users),get/setMyDefaultAdministratorRights.getChatalso returnshas_protected_contentandfalcon_join_by_request;protect_contentworks in everysend*method. - Links:
https://falconblack.org/bot/<username>?startgroup=<parameter>and?startchannel, with&admin=rights joined by+(change_info,delete_messages,restrict_members,invite_users,pin_messages,promote_members,anonymous,post_messages,edit_messages,manage_chat). With a parameter the program also gets/start@<username> <parameter>from the person who added the bot. - Updates:
my_chat_memberalways;message,edited_message,channel_post,edited_channel_post,callback_query;chat_memberonly for an admin bot that asked for it inallowed_updates(via_join_requestafter an approved request);chat_join_requestonly for an admin bot withcan_invite_usersthat listed it inallowed_updates— the person's number at this bot, name and language, andinvite_linkonly for the bot's own link; no username,user_chat_idor bio. - Limits: 20 messages a minute per bot per group, 60 a minute for all bots of a group, 20 posts a minute per bot per channel; 10 bots per group, 5 per channel; 10 000 pending updates per bot.
- What members see: a “bot” badge and “sees all messages” / “doesn't see messages” in the member list; a bot reply in a group notifies only the person it replies to or mentions; channel posts by bots notify subscribers at most once per 30 seconds.
Mini apps
A mini app is your website opened inside Falcon Black: as a sheet over the chat, full height, or full screen for games. People open it from the bot's profile, a button under a message, the button next to the input field, a keyboard cell or a link. On first launch the person allows the site to get their name and language; the site doesn't get their username, phone number, contacts or chats. Falcon Black always shows the app name and the site address on top; the page can't cover them.
- Register in @botnest:
/newapp(bot → name → short name →https://URL → description → icon → privacy policy URL → age rating),/myapps,/editapp,/deleteapp; or in the new apps: “My bots” → “Mini apps”. URL:https, port 443, a host name on the public internet; Falcon Black and local addresses are refused; up to 5 other site hosts. - Links:
https://falconblack.org/bot/<bot username>/<short name>, optionally?startapp=<parameter>(up to 512 ofA-Z a-z 0-9 _ -, arrives asstart_param); the bot's main app —…/bot/<username>?startapp. Bots may send such links. A new bot's mini apps open only for its owner during the first 3 days. - SDK:
window.FalconBlack.WebApp(alsowindow.Falcon.WebApp); methods return promises and fail withFalconWebAppErrorand acode.ready,close,expand,requestFullscreen,MainButton,BackButton,showPopup/showAlert/showConfirm,HapticFeedback,themeParamsand--fb-theme-*CSS variables,viewportHeight,safeAreaInset,openLink,openFalconLink,sendData,shareToChat,requestWriteAccess,onEvent. No camera, microphone, location, contacts or in-app payments in this version. Example: miniapp-example.html.
<script src="https://falconblack.org/sdk/mini-app-1.0.0.js" integrity="sha384-iPxcV8KtBhjKcb6hdeKa5FnZRrlW+kJU+AYoR4BKChP9HgNjye3Giaz2muZbzZ8B" crossorigin="anonymous"></script>
initData
Send WebApp.initData to your server in the request body (never in a URL) and validate it there.
Fields: user (JSON: id — the person's number for your bot, first_name,
language_code if known, allows_write_to_pm), auth_date, query_id,
chat_type and chat_instance (only when opened from the chat with the bot),
start_param, falcon_app (short name), signature, hash. Never a
username, photo, phone number, real id or chat titles.
- Data-check string: all fields except the excluded ones, sorted by name,
name=valuewith decoded values, joined by\n; parse with a form decoder only; a repeated field — reject. - hash:
secret = HMAC_SHA256(key = "WebAppData", msg = <full bot key>),hash = hex(HMAC_SHA256(secret, string without hash)); compare in constant time. Present once the live key has made any Bot API request (or after issuing a new key). - signature: Ed25519 over
"<bot id>:WebAppData\n" + string without hash and signature, base64url; public keys at /sdk/keys.json (try all of them). No bot key needed. - Accept
auth_dateup to one hour old (hard limit 24 h).WebAppDatais a fixed constant, not a name, so existing validation libraries work unchanged.
Ready-made validators: validate-initdata.mjs (Node.js 18+), validate-initdata.py (Python 3.9+); test vectors: initdata-vectors.json.
import { validateInitData, fetchPublicKeys } from "./validate-initdata.mjs";
const keys = await fetchPublicKeys(); // /sdk/keys.json: держите в памяти, обновляйте раз в час
// initData — из ТЕЛА запроса страницы, не из адреса
const r = validateInitData(initData, { botToken: process.env.FB_BOT_TOKEN, publicKeys: keys });
if (!r.ok) { res.writeHead(401).end(); return; }
const person = r.data.user; // {id, first_name, language_code?, allows_write_to_pm}
web_app buttons, menu button, data
{"chat_id": 4815162342108, "text": "Каталог открыт",
"reply_markup": {"inline_keyboard": [[{"text": "Открыть магазин", "web_app": {"url": "https://shop.example.com/item/5"}}]]}}
{"chat_id": 4815162342108, "text": "Что посмотрим?",
"reply_markup": {"keyboard": [[{"text": "Каталог", "web_app": {"url": "https://shop.example.com/catalog"}}, "Помощь"]],
"resize_keyboard": true}}
web_appbuttons (inline and keyboard) work only in one-to-one chats; groups and channels get 400webapp_in_group— use aurlbutton with the app link. The URL must belong to your registered app (its host or one of its other hosts), otherwise 400webapp_not_registered; a bot key in the URL revokes the key (key_leak). Older app versions show the inline button as a plain link (the site opens in a browser, without signing in) or, without buttons, as a numbered line with the app link.- A mini app opened from a keyboard cell may call
sendDataonce (up to 4096 bytes): the bot gets amessagewithweb_app_data(notext) by polling or webhook. answerWebAppQuery(only for apps opened from the chat with the bot, once, within an hour): an article withmessage_textbecomes one regular bot message to the person; returns{"falcon_message_id": N}. Errors:query_invalid,query_no_chat,cant_initiate(opening a mini app doesn't let the bot write first).
Rules for mini app authors
- Don't imitate the Falcon Black interface; never ask for a Falcon Black password or login code.
- Don't sell digital goods or services to iPhone users (App Store rule 3.1.1).
- A privacy policy is required; delete a person's data on request; state the age rating honestly.
- Every app can be reported from inside it. Break the rules — the app is turned off; again — the bot too.
Rules for bot authors
- A bot does not pretend to be Falcon Black, another person or organization.
- A bot does not send ads or write to people who didn't ask.
- A bot does not trick people into giving passwords, login codes, card details or money.
- What people write to a bot belongs to them. Keep it safe, don't show it to anyone, delete it on request.
- The key is like a password. We never ask for it.
- The owner is responsible for the bot. Break the rules — the bot is disabled; again — no more bots.
In full — falconblack.org/bots/rules.
A bot's privacy policy: add its address in My bots → bot → Bot settings; without it, the standard Falcon Black privacy policy for bots applies.