Клиентский API
Интерфейс, через который работают приложения Yobi. Любой может написать собственный клиент.
Общие правила
Каждый метод вызывается запросом POST https://yobi.chat/api/v1/<метод> с JSON-телом. Методы, кроме входа, требуют заголовок Authorization: Bearer <токен сессии>.
Успешный ответ:
{"ok": true, "result": { ... }}
Ошибка:
{"ok": false, "error_code": 400, "error": "PHONE_INVALID", "description": "неверный номер телефона"}
error — машиночитаемый код, по нему клиент решает, что делать. При превышении лимита приходит код 429 FLOOD_WAIT с полем retry_after — через сколько секунд можно повторить.
Ограничение по умолчанию — 30 запросов в секунду на сессию с запасом 120. Даты — unix-время в секундах. Смещения форматирования (entities) считаются в кодовых единицах UTF-16, как индексы строк в JavaScript, Java и Swift.
Вход
auth.sendCode{"phone": "+79001234567"}— отправить код. Если у аккаунта есть активные сессии, код придёт сообщением от служебного аккаунта «Yobi» в приложение (delivery: "app"), иначе по SMS. Повторно — черезtimeoutсекунд;force_sms: true— принудительно по SMS.auth.signIn{"phone", "code"}— ответ содержит одно из трёх:
tokenиuser— вход выполнен;need_signup: true— номер новый, вызовитеauth.signUp;password_required: true— включён облачный пароль, вызовитеauth.checkPassword.
auth.signUp{"phone", "code", "first_name", "last_name"}— регистрация.auth.checkPassword{"phone", "code", "password"}— вход с облачным паролем.
Заголовок X-Yobi-Device задаёт название устройства в списке сессий, например Yobi для Android, Pixel 8.
| Метод | Параметры | Результат |
|---|---|---|
auth.logOut | — | завершить текущую сессию |
auth.getSessions | — | список Session |
auth.terminateSession | session_id (0 — все, кроме текущей) | true |
auth.setPassword | current_password, new_password (пусто — отключить), hint | true |
auth.hasPassword | — | {"has_password": bool} |
auth.deleteAccount | — | true |
Обновления в реальном времени
Откройте WebSocket https://yobi.chat/api/v1/ws?token=<токен> (схема ws:// или wss://). Сервер присылает кадры двух видов:
{"pts": 42, "update": {"type": "new_message", ...}} — сохраняемое событие
{"update": {"type": "typing", ...}} — эфемерное событие
У каждого пользователя есть счётчик pts, он растёт на 1 с каждым сохраняемым событием. Клиент хранит последний применённый pts:
- пришёл кадр с
pts= свой + 1 — применить и запомнить; - пришёл кадр с
ptsбольше, чем свой + 1 — часть событий пропущена: вызватьupdates.getDifference{"pts": свой}и применить полученное; - после переподключения первый кадр
helloсодержитstate.pts— если он больше своего, вызватьupdates.getDifference.
Если getDifference вернул too_long: true, пропущено слишком много — заново загрузите список чатов (chats.getDialogs). Пока final: false, повторяйте запрос с новым pts.
Клиент может отправлять в сокет:
{"type": "ping"}— сервер ответитpong;{"type": "typing", "chat_id": 1, "action": "typing"}— «печатает…»; действия:typing,record_voice,upload_photo,upload_video,upload_document,cancel;{"type": "presence", "active": false}— приложение свёрнуто (пользователь перестаёт быть «в сети»);{"type": "call_signal", "call_id": 1, "data": {...}}— сигнал WebRTC (SDP, ICE) другой стороне звонка.
Типы событий
| type | Поля | Значение |
|---|---|---|
new_message | message, users | новое сообщение |
edit_message | message | сообщение изменено |
delete_messages | chat_id, ids | сообщения удалены |
read_inbox | chat_id, max_id | вы прочитали чат на другом устройстве |
read_outbox | chat_id, max_id, reader_id | собеседник прочитал ваши сообщения |
chat | chat | чат изменился (название, права, закреп и т. д.) |
chat_removed | chat_id, kicked, deleted | чат пропал из списка |
history_cleared | chat_id, max_id | история очищена |
reactions | chat_id, msg_id, reactions | изменились реакции |
typing | chat_id, user_id, action | кто-то печатает |
user_status | user_id, online, last_seen | статус «в сети» |
user | user | профиль изменился |
callback_answer | query_id, text, show_alert, url | ответ бота на нажатие кнопки |
sessions_changed | — | список сессий изменился |
poll | chat_id, msg_id, poll | изменились итоги опроса (эфемерное) |
stickers_changed, gifs_changed | — | изменились наборы стикеров / сохранённые GIF |
comments | chat_id, msg_id, comments | число комментариев к посту (эфемерное) |
topics_changed, topic_deleted | chat_id, topic_id | изменились темы форума |
read_thread | chat_id, thread_id, max_id | ветка прочитана на другом устройстве |
stories_changed | user_id или chat_id | у человека, канала или группы изменились истории (эфемерное) |
story_mention, story_new | story_id, user_id | вас отметили в истории; новая история человека с «Уведомлять о новых историях» |
story_collab | story_id, user_id, status | приглашение в совместную историю и ответ на него |
story_answer, story_reminder | story_id, area (+user_id) | ответ на «Вопрос» (автору); наступил обратный отсчёт («Напомнить») |
story_results, story_reaction, story_comments | story_id, area / emoji / count | изменились итоги стикеров, реакция, комментарии (эфемерные, автору) |
story_approval, stories_pending | story_id, chat_id, approved | решение по истории в группе; есть истории на одобрение (администраторам) |
stories_scheduled_changed, story_drafts_changed, highlights_changed | — | отложенные, черновики, «Актуальное» изменились на другом устройстве |
call_incoming, call_state, call_signal | call, data | звонки (эфемерные) |
secret_chat, secret_message | secret, message | секретные чаты (только устройству чата) |
search_recent_changed | — | «Недавние» в поиске изменились на другом устройстве — перезапросите search.getRecent |
Пользователи и контакты
| Метод | Параметры | Результат |
|---|---|---|
users.getMe | — | User |
users.getUser | user_id | User |
users.getUsers | ids (до 200) | словарь id → User |
users.updateProfile | first_name, last_name, bio (любые) | User |
users.setUsername | username (пусто — удалить) | User |
users.checkUsername | username, chat_id | {"available", "description"} |
users.setAvatar | file_id (пусто — удалить) | User |
users.getPrivacy / users.setPrivacy | last_seen, phone: everybody, contacts, nobody | настройки |
users.resolve | username | {"user"} или {"chat"} |
users.resolvePhone | phone | {"user"}; ссылка на номер yobi.chat/+79991234567, учитывает «Кто может найти меня по номеру»; ошибки PHONE_INVALID, PHONE_NOT_FOUND |
users.block | user_id, block | User |
contacts.get | — | список User |
contacts.add | user_id или phone, first_name, last_name | User |
contacts.delete | user_id | true |
contacts.import | contacts: [{phone, first_name, last_name}] | найденные User |
search.global | query | {"users", "chats"} |
Чаты
Тип чата: private (личный), group (группа, до 200 000 участников), channel (канал, без ограничений). Роли: owner, admin, member, left, kicked.
| Метод | Параметры | Результат |
|---|---|---|
chats.getDialogs | archived, offset, limit | {"chats", "users", "archived_count", "state"} |
chats.getChat | chat_id | {"chat", "users"} |
chats.getPrivate | user_id | личный чат (создаётся при необходимости) |
chats.createGroup | title, user_ids | Chat |
chats.createChannel | title, about, username | Chat |
chats.edit | chat_id, title, about, username, avatar_file_id, signatures, slow_mode_sec, permissions | Chat |
chats.addMembers | chat_id, user_ids | Chat |
chats.join | chat_id | вступить в публичный чат |
chats.leave | chat_id | выйти |
chats.kick | chat_id, user_id, ban | исключить |
chats.unban | chat_id, user_id | снять блокировку |
chats.restrict | chat_id, user_id, until (0 — снять) | запретить писать |
chats.promote | chat_id, user_id, rights (null — снять), title | назначить администратора |
chats.transferOwnership | chat_id, user_id, password | передать владение |
chats.getMembers | chat_id, filter (admins, banned, bots), query, offset, limit | {"members", "users"} |
chats.updateDialog | chat_id, pinned, archived, muted_until (-1 — навсегда, 0 — включить), draft | Chat |
chats.delete | chat_id, for_all | удалить чат |
chats.clearHistory | chat_id | очистить у себя |
chats.exportInvite | chat_id, renew | основная ссылка-приглашение |
chats.createInvite | chat_id, title, expires_at, usage_limit | дополнительная ссылка |
chats.getInvites | chat_id | список ссылок |
chats.revokeInvite | code | отозвать |
chats.checkInvite | code | что внутри ссылки |
chats.joinByInvite | code | вступить по ссылке |
Права администратора (rights): can_post, can_edit_others (каналы), can_delete, can_ban, can_invite, can_pin, can_change_info, can_promote.
Сообщения
| Метод | Параметры | Результат |
|---|---|---|
messages.getHistory | chat_id, offset_id, direction (before, after, around), limit | {"messages", "users", "replies"} |
messages.send | chat_id, text, entities или parse_mode, file_id, media_kind, reply_to_id, silent, schedule_at | Message |
messages.edit | chat_id, msg_id, text, entities | Message |
messages.delete | chat_id, ids, for_all | true |
messages.forward | from_chat_id, ids, to_chat_id, hide_author | список Message |
messages.react | chat_id, msg_id, emoji (пусто — снять) | Message |
messages.pin | chat_id, msg_id (0 — открепить), silent | true |
messages.read | chat_id, max_id | true |
messages.typing | chat_id, action | true |
messages.search | chat_id (0 — везде), query, filter (media, files, voice, audio, links), offset_id, limit (до 100) | страница сообщений (от новых к старым); в одном чате на первой странице (без offset_id) — ещё count: сколько всего найдено (для «N из M» в просмотрщике медиа) |
messages.pressButton | chat_id, msg_id, data | {"query_id"} |
messages.getScheduled / messages.deleteScheduled | chat_id / id | отложенные |
messages.report | chat_id, msg_id, user_id, reason, comment | жалоба |
Типы форматирования (entities[].type): bold, italic, underline, strikethrough, spoiler, code, pre (+language), blockquote, text_link (+url). Ссылки, @упоминания, #хэштеги и /команды сервер распознаёт сам. В превью вне пузыря (список чатов, закреп, цитата ответа, поиск, уведомления) клиент закрывает каждый spoiler отрезком ▒ той же длины в символах (не больше 12) и не даёт раскрыть; сервер так же маскирует текст пушей.
Файлы
Загрузка — POST https://yobi.chat/api/v1/upload, multipart/form-data:
file— содержимое;kind— необязательно:photo,video,voice,audio,document,sticker,animation,video_note;
для sticker — ещё sticker_target (regular, mask, custom_emoji, icon): сервер проверяет TGS или приводит картинку к PNG 512 px (эмодзи и обложка — 100×100);
duration,width,height— для медиа. У видео (MP4/MOV/WebM) сервер сам читает размеры кадра (с учётом поворота) из файла (они важнее присланных), а длительность — если её не прислали.
Ответ — объект Media с file_id. Его передают в messages.send. Скачать файл: GET https://yobi.chat/files/<file_id>, поддерживается Range для видео и аудио, ?download=1 — скачать как вложение.
Новые возможности
| Метод | Параметры | Результат |
|---|---|---|
messages.send с poll | poll: {question, options[], anonymous, multiple, quiz, correct_option, explanation, open_period} | сообщение-опрос |
polls.vote, polls.close, polls.getVoters | chat_id, msg_id, options[] | сообщение / голоса |
stickers.getInstalled {type?}, getRecent, getSet, install, search, reorderSets {type?, names} | type: regular (по умолчанию), mask, custom_emoji, gif | установленные наборы стикеров, масок, эмодзи и GIF |
stickers.createSet {title, name, set_type, format, stickers[{file_id, emoji, keywords?, mask_position?}], thumb_file_id?}, addToSet, replaceSticker, setStickerEmoji, setMaskPosition, setPosition, removeFromSet, setThumb, renameSet, deleteSet, getMine {type?}, checkName | name — короткое имя (общее для всех типов) | свои наборы (то же умеет бот @Stickers) |
stickers.getCustomEmoji {ids}, stickers.searchCustomEmoji {emoji} | до 200 id | свои эмодзи: entity custom_emoji с custom_emoji_id |
stickers.getStats, getTop, getSetStats, getSetTop, getTopInSet, getUsageTop | period: d7, d30, total | статистика своих наборов |
stickers.getEmojiCategories, getEmojiKeywords {lang?}, searchByEmoji {emoji[], type?, query?, limit?}, searchSets {query, type?, limit?} | lang: ru (по умолчанию) или en | панель эмодзи/стикеров/GIF: категории, ключевые слова, поиск |
stickers.setAccess {name, access, user_ids?, chat_ids?}, stickers.resolveAccessTarget {query} | access: all или selected | доступ к своему набору «Для всех» / «Для выбранных» |
gifs.getSaved, gifs.save, gifs.search, gifs.import | file_id, unsave, query, url | GIF (поиск — при YOBI_GIF_PROVIDER) |
chats.edit с is_forum | включить темы в группе | |
topics.get, topics.create, topics.edit, topics.delete | chat_id, topic_id, title, icon_emoji, closed, pinned | темы форума |
messages.getHistory / messages.read / messages.send с thread_id | работа с темой или веткой комментариев | |
chats.setDiscussion | channel_id, group_id | группа обсуждения канала |
messages.getDiscussion | chat_id, msg_id | группа и ветка комментариев к посту |
stories.post, getFeed, getUser, view, react, getViewers, edit, delete, getCloseFriends, setCloseFriends | file_id, caption, privacy, allowed, period_hours, pinned | истории (полный список — раздел «Истории» ниже) |
messages.send с reply_story_id | ответ на историю | |
inline.query, inline.send | bot_username, query, query_id, result_id | инлайн-боты |
webapp.init, webapp.sendData | bot_id, chat_id, url, source, start_param | запуск мини-приложения (только у запущенного бота — см. «Мини-приложения: сначала «Запустить»») |
payments.getForm, payments.pay | chat_id, msg_id, order_info, tip_amount | оплата счёта |
calls.getConfig, calls.request, calls.accept, calls.hangup, calls.signal, calls.getHistory | user_id, video, call_id | звонки |
secret.request, secret.accept, secret.discard, secret.list, secret.send, secret.getInbox, secret.ack | user_id, g_a/g_b (P-256, base64), payload | секретные чаты |
Версия 3: сообщения, чаты, аккаунт
Новые поля messages.send: geo {lat, lon, accuracy, title, address, live_period}, contact {phone, first_name, last_name, user_id}, checklist {title, items[], others_can_add, others_can_mark}, has_spoiler, view_once, quote {text, offset}, reply_to_chat_id, effect, no_link_preview, send_as, repeat_sec, when_online.
| Методы | Назначение |
|---|---|
messages.sendAlbum | альбом: items[{file_id, has_spoiler, text}] |
messages.openViewOnce, messages.toggleChecklist, messages.addChecklistItems, messages.editLiveLocation | одноразовые медиа, списки задач, трансляция геопозиции |
messages.getPinned, messages.unpin, messages.unpinAll | несколько закреплённых сообщений |
messages.getReadDate, messages.getByDate, messages.searchPublic, messages.getLinkPreview | когда прочитано, переход к дате, поиск по публичным каналам, превью ссылки |
messages.getGameUrl, messages.getGameScores, messages.sendQuickReply | игры ботов, быстрые ответы |
polls.addOption | вариант от участника |
chats.setTTL, chats.setNoForwards, chats.setWallpaper | автоудаление, запрет пересылки, обои |
chats.getAdminLog, chats.getStats, chats.getMessageStats, chats.getSimilar | журнал действий, статистика, похожие каналы |
chats.getJoinRequests, chats.resolveJoinRequest, chats.createInviteEx, chats.editInvite, chats.deleteRevokedInvites, chats.getInviteJoiners | заявки и ссылки-приглашения |
chats.ban, chats.restrictMember, chats.setMemberTag, chats.getMemberTags, chats.getSendAs | ограничения, метки участников, отправка от имени канала |
folders.get, save, delete, reorder, share, editInvite, getInvites, deleteInvite, checkInvite, joinInvite, hideInviteUpdates | папки (значок, цвет тега, типы и списки чатов), порядок вкладок (0 — «Все чаты»), ссылки /addlist/<code> с обновлениями; события folders_changed, folder_settings_changed; настройки вкладок — account.setSettings (folder_tags, folder_tabs_position, folder_tabs_style, folder_all_chats_index) |
privacy.getRules, privacy.setRule, users.getBlocked | приватность с исключениями |
users.updateProfileExtra, users.getPhotos, users.setMainPhoto, users.deletePhoto, contacts.setNote, contacts.getBirthdays | день рождения, эмодзи-статус, цвет, личный канал, фото, заметки; contacts.getBirthdays {tz_offset} — дни рождения контактов сегодня и в ближайшие 7 дней, «сегодня» по часовому поясу клиента (секунды к востоку от UTC) |
account.getSettings, setSettings, getNotifySettings, setNotifySettings, setEmail, verifyEmail, registerPush, unregisterPush | настройки, почта, push (kind: fcm, apns, unifiedpush) |
auth.exportLoginToken, checkLoginToken, acceptLoginToken | вход по QR-коду |
auth.requestPasswordRecovery, auth.recoverPassword | сброс облачного пароля через почту |
auth.passkeyOptions, auth.passkeyLogin, passkeys.registerOptions, register, list, delete | ключи доступа (WebAuthn) |
groupCalls.create, start, subscribe, join, leave, signal, setState, moderate, edit, end, getMembers, react | видеочаты и трансляции (mesh WebRTC) |
business.get, set, getQuickReplies, saveQuickReply, deleteQuickReply, getLinks, saveLink, deleteLink, resolveLink, connectBot, disconnectBot | бизнес-профиль |
stickers.getFavorites, stickers.fave, stories.getChat, stories.getAlbums, stories.setAlbum, apps.getCatalog, apps.getAttachMenu | избранные стикеры, истории каналов и альбомы, каталог мини-приложений |
Оформление чатов (миграция 021):
chats.setWallpaper {chat_id, wallpaper, for_both}—wallpaper:{kind: color|gradient|image|theme, color, color2 (#rrggbb), file_id (для image), dark_dim (0..1), blur, pattern, theme},{}— сбросить;urlкартинки сервер вычисляет сам. В личном чате с человеком — для себя или для обоих; в группе и канале обои общие, их меняют только владелец и администраторы сcan_change_info(иначе 403CHAT_ADMIN_REQUIRED); в чате с ботом — всегда 403CHAT_ADMIN_REQUIRED. Изменение приходит всем событиемchat.chat.wallpaper_locked— зритель не может менять обои (скрыть пункт «Обои»).chat.accent_color— цвет названия группы или канала, индекс палитры#E5544B, #F08A24, #D9A400, #3FB15D, #14A3A3, #2E7BF0, #7F5AF0, #C94FC9, #E0529C, #6B7A8F; нет поля — цвет по id. Меняется черезchats.edit {accent_color}(-1 — сбросить).bots.setWallpaper {bot_id, wallpaper}→{wallpaper}— владелец бота задаёт обои, которые видят все собеседники бота;bots.getMyBotsвозвращаетwallpaper.chats.getManageCounts {chat_id}→{admins, members, banned, invites, join_requests, reactions}— для экрана «Управление каналом/группой» (администраторам).- В канал нельзя отправить
geo,contactиchecklist: 400MEDIA_NOT_ALLOWED.
Видеочат: новичок шлёт SDP-предложение каждому участнику через groupCalls.signal (data: {type: offer|answer|ice, …}), события приходят как group_call, group_call_member, group_call_signal, group_call_ended.
Версия 4: настройки
| Методы | Назначение |
|---|---|
account.sendChangePhoneCode, account.changePhone | смена номера: код по SMS на новый номер |
account.setSettings с hide_read_time, find_by_phone | скрыть время прочтения; кто может найти по номеру (everybody, contacts) |
account.resetNotifySettings | сбросить все настройки уведомлений и исключения |
account.registerPush с kind: webpush | подписка браузера (JSON PushSubscription); ключ VAPID — account.getPushConfig().webpush_key |
contacts.deleteImported | удалить контакты, загруженные из телефонной книги |
messages.clearAllDrafts | удалить все черновики |
messages.getScheduled, getScheduledCounts, editScheduled, sendScheduledNow, deleteScheduled | отложенные сообщения |
stickers.reorderSets | порядок наборов стикеров |
Умный поиск
Постраничные методы принимают offset (строка из next_offset прошлой страницы; в первый раз — пусто) и возвращают next_offset (нет поля — больше ничего нет). Страница сообщений: {"messages", "users", "chats", "next_offset"}, новые сверху.
| Метод | Параметры | Результат |
|---|---|---|
search.getRecent | limit (20, до 50) | {"recent": [{"user_id" или "chat_id", "date"}], "users", "chats", "apps"}; apps — {bot_id: BotApp} для ботов с мини-приложением (кнопка «Открыть», monthly_users) |
search.addRecent / search.removeRecent | user_id или chat_id | true; хранится 50 последних, общие для всех устройств |
search.clearRecent | — | true |
messages.searchGlobal | query, filter, offset, limit (40, до 100) | сообщения из всех чатов, где вы участник (без секретных, удалённых у себя и очищенной истории). filter: "" (текст, query обязателен), photo, video (с GIF), photo_video, document, link, music, voice (с кружками). С фильтром query необязателен и ищется по подписи, имени файла, адресу и заголовку ссылки |
search.posts | query (от 2 символов), offset, limit (30, до 50) | посты публичных каналов — глобальный поиск, бесплатно для всех |
search.channels | limit (20, до 50) | {"my_channels", "recommended"}: свои каналы (свежие сверху) и популярные публичные, на которые вы не подписаны |
search.apps | query, offset, limit (20, до 50) | {"apps": [BotApp], "users", "next_offset"}; BotApp: bot_id, url, users_count, monthly_users (открывали за 30 дней), featured |
Истории
Формат истории (холст 9:16, kind media/text/collage, фон, коллаж, правки, слои, музыка, голос, интерактивные области и их results) — единый контракт в docs/stories.md репозитория; там же все уточнения сервера. Старые клиенты, которые шлют только file_id, caption, areas, работают как раньше. Ошибки проверки содержимого — STORY_CONTENT_INVALID, STORY_AREAS_INVALID, STORY_MEDIA_INVALID, STORY_VIDEO_TOO_LONG, FILE_ID_INVALID.
| Методы | Назначение |
|---|---|
stories.post, postMany {items ≤ 10}, edit, delete, repost {story_id / chat_id+msg_id} | публикация: новые поля kind, background, collage, edits, layers, music, voice, noforwards, reposts_allowed, view_once, excluded (privacy=except), collab_user_id, scheduled_at, repost, chain_id, comments_enabled, group_id, period_hours до 168 |
stories.getFeed, getUser, getChat, getByIds, view (→ {expired_ids}), react, track {event: complete/exit/link/sticker} | лента (close_friends у автора, stealth_until), просмотр, одноразовые истории |
stories.vote, slide, answer, getAnswers, remind, going, getVoters | интерактивные стикеры: опрос, викторина, слайдер, вопрос, обратный отсчёт, событие |
stories.getViewers {filter: all/contacts/reactions/voted, query, offset}, getStats, getSeriesStats {date} | зрители (с ответами на стикеры), статистика и удержание серии дня |
stories.collabRespond, getChain, searchHashtag, getMap {lat, lon, radius_km} | совместные истории, «Добавь своё», хештеги, карта историй |
stories.getScheduled, cancelScheduled, getDrafts, saveDraft, deleteDraft | отложенные и черновики (на всех устройствах) |
stories.getHighlights, getHighlightStories, saveHighlight, deleteHighlight, reorderHighlights, getArchive {month}, getMemories | «Актуальное» (старые getAlbums/setAlbum работают поверх него), календарь архива, «В этот день» |
stories.hideAuthor, getHidden, setStealth, setNotify, getNotify | скрыть автора из ленты, незаметный просмотр (25 минут), «Уведомлять о новых историях» |
stories.getComments, addComment, deleteComment | комментарии к историям каналов |
chats.edit {stories_mode: off/on/approval}, stories.getPending, stories.approve | истории участников в группах и их одобрение |
stories.report, messages.report {story_id}; админка: admin.storyAction {story_id, action: delete/hide_map/show_map} | жалобы на истории (chat_type: story в очереди, решение hide_map) |
bots.allowStories, bots.getStoryBots, apps.shareToStory {bot_id, file_id, text, link}; Bot API postStory, deleteStory | истории от ботов с разрешения пользователя; мини-приложение создаёт черновик |
Секретные чаты
Каждое устройство создаёт пару ключей ECDH P-256 и передаёт открытый ключ (несжатая точка, 65 байт, base64). Общий ключ: ECDH → HKDF-SHA256(salt="yobi-secret-<id>", info="message-key") → AES-256-GCM. payload — base64(iv 12 байт ‖ шифротекст) от JSON {"v":1,"type":"msg"|"read"|"delete"|"ttl","rid":"…","date":…,"text":"…","media":{…},"ttl":…}. Файлы шифруются отдельным случайным ключом AES-GCM и загружаются как secret.bin; ключ и iv передаются внутри сообщения. Сервер удаляет сообщения после secret.ack.
Звонки
calls.request → собеседнику call_incoming → calls.accept → звонящий создаёт SDP-предложение, стороны обмениваются call_signal ({"sdp": …} и {"candidate": …}). STUN/TURN — из calls.getConfig (TURN включается переменными YOBI_TURN_URLS и YOBI_TURN_SECRET для coturn с use-auth-secret).
Мини-приложения: сначала «Запустить»
Мини-приложение открывается только у бота, которого пользователь запустил сам. Запущен — у пользователя есть личный чат с ботом (он не вышел из него), в этом чате есть хотя бы одно сообщение от пользователя (/start из bots.start или любое другое — старые диалоги считаются запущенными) и пользователь не заблокировал бота. Сообщения от бота (уведомления, например от @Usernames) запуском не считаются. Исключений нет — владелец бота тоже нажимает «Запустить».
Проверка на сервере: webapp.init, activities.launch (и activities.invite / activities.answer accept) и вход мини-приложения @Usernames по заголовку X-Yobi-Init-Data отвечают ошибкой 403 BOT_NOT_STARTED, data: {bot_id, username}, если бот не запущен (уже выданные initData перестают работать, если пользователь удалил чат или заблокировал бота).
webapp.init {bot_id, chat_id, url, source, start_param?} → {url, init_data, query_id?, bot_id}. start_param — из ссылки <адрес>/<бот>?startapp=<параметр> (латиница, цифры, _, -, до 64 символов, иначе START_PARAM_INVALID); попадает в подписанные initData полем start_param. Успешный webapp.init засчитывается как запуск приложения в статистике бота; apps.logLaunch клиент вызывает после того, как приложение действительно открылось.
Поведение клиента (все входы: кнопки под сообщением и клавиатуры, кнопка меню, «скрепка», Apps «Открыть», «Открыть» в списке чатов, страница приложения, поиск, ссылки, поля имени в профиле/канале/группе, «Юзернеймы» в настройках):
- Перед открытием закрыть окна, настройки и экран управления чатом — мини-приложение не должно оказаться под ними.
- Вызвать
webapp.init(илиactivities.launch). Успех — открыть приложение. BOT_NOT_STARTED— не открывать приложение и не отправлять /start за пользователя: открыть личный чат с ботом (chats.getPrivate), показать внизу кнопку «Запустить» и над ней подсказку «Нажмите «Запустить», чтобы открыть «<название>»»; запомнить запрошенный запуск (url, source, chat_id, start_param, сессию игры, название).- Пользователь нажимает «Запустить» →
bots.start {bot_id}(сообщение/startот пользователя, чат появляется в списке) → повторить шаг 2 с запомненными параметрами — приложение откроется само. Переход в другой чат отменяет ожидание. - Ссылка
<адрес>/<бот>?start=<параметр>открывает чат с ботом и кнопкой «Запустить», которая отправляетbots.start {bot_id, param};?startapp[=<параметр>]— главное мини-приложение бота по шагам 1–4.
Статьи
Сообщение kind="article" — документ из блоков, как «Статья» в Telegram. Отправка: messages.send {chat_id, article} (без text, file_id, poll и других вложений); правка автором или администратором канала с правом изменять чужие посты: messages.edit {chat_id, msg_id, article} — статья заменяется целиком, text для статьи не принимается. Пересылка сохраняет статью. В сообщении приходит поле article, а text равен заголовку; превью в списке чатов и уведомлениях — «📰 заголовок». Поиск (messages.search, messages.searchPublic) ищет и по тексту статьи.
article: title (до 256 символов; если пуст, заголовком становится первый блок heading), cover {file_id}, blocks[]; сервер добавляет excerpt (до 300 символов для карточки), read_minutes и photo (объект Media) у обложки и картинок. Блок: type и поля по типу — heading {level 1–3, text, entities}, paragraph/quote/pullquote/small {text, entities}, code {text, language}, divider, list {ordered, items[{text, entities}]}, checklist {items[{text, entities, checked}]}, details {text, entities, open, blocks[]} (до 3 уровней), table {header, rows[[{text, entities}]]} (до 100×20), image {file_id, text, entities} (подпись), formula {tex}. Текст — как в сообщениях (смещения UTF-16); дополнительно разрешены entities subscript, superscript, marked. Картинки — file_id из /api/v1/upload (фото или GIF). Ограничения: 100 КБ JSON, 50 картинок, 1000 блоков, 10000 символов в блоке. Ошибки: ARTICLE_INVALID, ARTICLE_TOO_LONG, FILE_ID_INVALID.
Объекты
User: id, first_name, last_name, username, bio, phone (если видно по приватности), avatar, is_bot, is_system, is_contact, is_self, is_blocked, status (online, offline, recently, long_ago, bot, service), last_seen, bot_commands, bot_menu_button, bot_main_app_url (главное мини-приложение бота; пусто — нет), bot_list_button (показывать кнопку «Открыть» в строке бота в списке чатов: есть главное мини-приложение и владелец не выключил её; видна только при Chat.bot_started; нажатие открывает bot_main_app_url через webapp.init, apps.logLaunch source="chatlist").
Chat: id, type, title, about, username, avatar, peer_id, member_count, my_role, rights, permissions, last_message, unread_count, unread_mentions, read_inbox_max, read_outbox_max, pinned_msg_id, muted_until, pinned, archived, draft, slow_mode_sec, signatures, sort_date, bot_started (личный чат с ботом: зритель уже запустил бота; кнопку «Открыть» из bot_list_button показывать только при bot_started, пока бот не запущен — «Запустить» в чате; меняется событием chat после bots.start, первого сообщения боту и блокировки/разблокировки).
Message: chat_id, id (растёт внутри чата), sender_id, date, kind (text или service), text, entities, media, reply_to_id, forward, reply_markup, author_signature, service, views, edit_date, reactions, out.
Media: file_id, kind, name, mime, size, width, height, duration, url.
Ссылки
https://yobi.chat/<username>— профиль, канал или группа;https://yobi.chat/<username>/<id>— сообщение в публичном чате;https://yobi.chat/+<код>— ссылка-приглашение;https://yobi.chat/addlist/<код>— папка с чатами;https://yobi.chat/m/<код>— бизнес-ссылка на чат с готовым текстом;https://yobi.chat/<бот>?start=<параметр>— чат с ботом, «Запустить» отправит/start <параметр>;https://yobi.chat/<бот>?startapp=<параметр>— главное мини-приложение бота (start_param), у незапущенного бота — сначала «Запустить»;yobi://login?token=<токен>— вход по QR-коду.yobi://resolve?phone=79991234567иhttps://yobi.chat/+79991234567— профиль по номеру телефона (QR-код профиля без имени пользователя).