Подключите своё веб-приложение к мессенджеру X2Chat. Мини-апп открывается во встроенном WebView, получает бесшовный вход по аккаунту пользователя, может запрашивать данные через защищённый Host API и отправлять карточки-ссылки в чат.
Проще всего — раздел разработчика: зайди по QR из своего X2Chat, заполни форму и опубликуй. Там же — список приложений со статусом, метрики и удаление. Без токенов и команд: /miniapps/dev/
Удаление — только по QR-подтверждению: удалить приложение можно лишь отсканировав QR в X2Chat со своего телефона-владельца — чужой удалить не сможет.
Мини-апп — это размещённый по HTTPS веб-фронтенд (HTML/JS/CSS, любой стек). X2Chat хранит о нём запись в реестре (slug, name, icon_url, launch_url, allowed_origins, available_scopes) и открывает launch_url во WebView. Приложение может быть полностью самостоятельным (свой бэкенд, своя БД, свой бренд) — X2Chat выступает «лаунчером» и поставщиком идентичности пользователя.
mini_app_launch, ~5 минут) и заголовок X-X2Chat-Mini-App-Session.
Подключение полностью self-service: размещаете манифест на своём домене и шлёте одну заявку. Подключать вас вручную никто не должен.
https://ru.x2chat.com/apps/<slug>/ (см. §8).POST /api/v1/mini-apps/register из своего аккаунта X2Chat.Файл x2chat-manifest.json в корне приложения (на том же домене, что и launch_url — это и есть подтверждение, что приложение ваше):
{
"slug": "otc",
"name": "OTC Desk",
"description": "OTC-сделки и котировки",
"launch_url": "https://ru.x2chat.com/apps/otc/",
"scopes": ["profile:read"]
}
Иконку положите рядом — icon.png (см. §9), отдельно указывать не нужно.
Из аккаунта X2Chat (нужен ваш токен авторизации; запрос идёт от вас — вы становитесь владельцем приложения):
curl -X POST https://ru.x2chat.com/api/v1/mini-apps/register \
-H "Authorization: Bearer <ВАШ_ТОКЕН_X2CHAT>" \
-H "Content-Type: application/json" \
-d '{"manifest_url":"https://ru.x2chat.com/apps/otc/x2chat-manifest.json"}'
Что делает сервер сам: тянет манифест → сверяет, что домен ваш → проверяет, что launch_url отвечает (200), есть icon.png, запрошенные права поддерживаются → принимает решение:
profile:read/chat:context/message:send) → принято и ждёт одобрения оператора (HTTP 202).Запись создаётся на обоих дата-центрах автоматически. Обновить приложение (имя/иконку/права) — поменяйте манифест и повторите ту же команду.
Не хотите возиться с токеном? Сгенерируйте QR-код и отсканируйте его в приложении X2Chat — приложение подключится без токена: /miniapps/qr/ (требует свежей версии приложения X2Chat).
После загрузки страницы X2Chat инжектит window.X2Chat.launch и шлёт событие x2chat:ready:
window.addEventListener('x2chat:ready', (e) => {
const { sessionToken, context } = window.X2Chat.launch;
// context = { app_id, slug, user_id, slot_id, chat_id, scopes, theme, locale }
// slot_id — crypto-идентификатор юзера (null для обычных аккаунтов)
applyTheme(context.theme); // напр. 'blue'
applyLocale(context.locale); // напр. 'ru'
});
Если событие уже прошло к моменту подписки — проверьте наличие window.X2Chat?.launch напрямую.
| Вызов | Назначение |
|---|---|
window.X2Chat.postMessage({ method: 'close' }) | Закрыть мини-апп. |
window.X2Chat.postMessage({ method: 'sendMessageDraft', payload: { text } }) | Вставить текст черновиком в текущий чат и закрыть мини-апп (пользователь подтверждает и отправляет сам). |
window.X2Chat.postMessage({ method: 'getContext' }) | Повторно инжектит window.X2Chat.launch. |
await window.x2.requestCapability(scope, payload) | Запрос к Host API (Promise). Внутри идёт через мост с requestId. |
window.X2Chat.postMessage({ method: '_refreshHandled' }) | ACK на pull-to-refresh — мини-апп сам обновил текущую страницу (см. §3.1). Гасит спиннер хоста и отменяет полную перезагрузку. |
await window.x2.requestCapability('message:notify', { to:{slot_id}, text }) | Отправить получателю по slot_id E2E-сообщение (пуш придёт штатно). Гейт по scope message:send (см. §3.5). |
await window.x2.requestCapability('session:refresh', {}) | Форсировать переиздание launch-сессии (token-broker) — обновляет sessionToken в контейнере (см. §3.6). |
Картинки/вложения мост не принимает — только текст. Для «картинки-карточки» используйте превью ссылок (§6).
Когда пользователь тянет контейнер мини-аппа вниз (на вершине прокрутки), X2Chat не перезагружает весь WebView — это выкидывало бы из SPA и сбрасывало навигацию. Вместо этого хост шлёт в страницу событие x2chat:refresh, а мини-апп сам обновляет свою ТЕКУЩУЮ страницу и подтверждает это.
window.addEventListener('x2chat:refresh', async () => {
try {
await refreshCurrentPage(); // ваш re-fetch/re-render активного экрана
} finally {
// ОБЯЗАТЕЛЬНО: иначе через 2 с хост перезагрузит весь WebView (fallback)
window.X2Chat?.postMessage?.({ method: '_refreshHandled' });
}
});
Совместимость. Если ACK _refreshHandled не пришёл за 2 секунды, хост делает полный reload() — старые мини-аппы, не знающие про x2chat:refresh, продолжают работать (просто без постраничности). Доступно с контейнера iOS 2.27.4+1442.
Кнопка/жест «назад» по умолчанию закрывает мини-апп. Чтобы «назад» сначала ходил по внутренней иерархии мини-аппа (деталь → список → корень) и закрывал его только с корневого экрана, мини-апп сообщает хосту своё навигационное состояние при каждой смене экрана:
function syncNav() { window.x2.setBackState(router.canGoBack(), hasUnsavedChanges()); }
// вызывать syncNav() на каждой смене экрана и на x2chat:ready
window.addEventListener('x2chat:back', () => { router.back(); syncNav(); });
window.addEventListener('x2chat:exitRequest', () => {
if (confirm('Выйти из приложения?')) window.X2Chat.postMessage({ method: 'close' });
});
| Сигнал | Смысл |
|---|---|
window.x2.setBackState(canGoBack, confirmExit) | Мини-апп → хост: можно ли идти назад внутри (canGoBack) и нужно ли подтверждать выход (confirmExit). |
событие x2chat:back | Хост → мини-апп: уйти на предыдущий экран по своей иерархии (приходит при canGoBack=true). |
событие x2chat:exitRequest | Хост → мини-апп: пользователь хочет выйти, а вы выставили confirmExit=true — покажите подтверждение и на согласие вызовите postMessage({method:'close'}). |
iOS: при наличии «назад»-цели нативный edge-swipe подавляется — внутренний «назад» через шеврон в шапке или свой UI; на корне свайп закрывает мини-апп штатно. На Android системный жест/кнопка делегируются всегда. Дефолт (мини-апп не шлёт состояние) — «назад» сразу закрывает. Доступно с iOS 2.27.4+1442.
Камера и микрофон внутри мини-аппа работают через стандартный navigator.mediaDevices.getUserMedia — отдельный мост не нужен. X2Chat выдаёт WebView веб-разрешение на медиа автоматически. Типичный сценарий — сканер QR внутри приложения.
// задняя камера для сканера
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' } }, audio: false
});
videoEl.srcObject = stream; await videoEl.play();
// декодировать QR любой библиотекой: @zxing/browser | html5-qrcode | jsQR
stream.getTracks().forEach(t => t.stop()); // по завершении — ОБЯЗАТЕЛЬНО погасить
Требования и совместимость. Только HTTPS (на http getUserMedia не работает); камеру дёргать по действию пользователя (клик), не на загрузке. На Android доступ к камере во WebView выдаётся с контейнера после фикса 2026-06-19 — на старых сборках сделайте мягкий fallback (ручной ввод / вставка из буфера) на ошибки NotAllowedError / NotFoundError / NotReadableError и проверяйте navigator.mediaDevices?.getUserMedia перед вызовом. На iOS работает по системному запросу доступа (первый запуск спросит).
Для крипто-кошельков и приложений, которым нужна идентичность собеседника или системное «поделиться» — через window.x2.requestCapability(scope, payload):
| scope | payload | Что делает | Возврат |
|---|---|---|---|
share:external | { text, url, title } | Системный share-лист (Android Intent / iOS UIActivityViewController). Без отдельного права. | { success: true } |
share:forward | { text, url } | Пикер чатов X2Chat → отправляет текст выбранному контакту (E2E через ChatService). Без отдельного права. | { success: true, sentTo: [chat_id] } / { success: false, error } |
contacts:pick | {} | Пикер контактов → выбранный собеседник (1:1). Нужен scope contacts:pick. Для группы slot_id=null, group=true. | { user_id, slot_id, display_name, group } / { cancelled } |
// поделиться системно
await window.x2.requestCapability('share:external', { text: 'TRC20 адрес', url: 'https://…' });
// выбрать получателя (нужен scope contacts:pick)
const c = await window.x2.requestCapability('contacts:pick', {});
if (!c.cancelled && c.slot_id) resolveAndSend(c.slot_id, c.display_name);
Идентичность: slot_id текущего юзера приходит в context.slot_id (см. §3); contacts:pick отдаёт slot_id собеседника (раскрытие → требует scope, уходит на одобрение оператора). Совместимость: доступно с Android-сборки после 2026-06-19; на старых сборках requestCapability вернёт ошибку — предусмотрите fallback (ручной ввод / буфер).
slot_id (message:notify)Хост резолвит/создаёт 1:1-чат с получателем по slot_id и отправляет ему E2E-сообщение text. Пуш приходит штатно — как на любое новое сообщение, отдельный notify слать не нужно. Тело сообщения = текст пуша. Главный сценарий — кошелёк, уведомляющий получателя перевода.
const r = await window.x2.requestCapability('message:notify', {
to: { slot_id: 'YQS4C0' }, // получатель по slot_id (из contacts:pick / формы)
text: '💸 Перевод 2 USDT' // тело сообщения = текст пуша
});
// → { success: true, chat_id, message_id }
// → { success: false, error: 'recipient_not_found' | 'no_permission' | 'empty_text' }
Гейт по уже выданному scope message:send — отдельный scope не заводите (иначе нужна переустановка мини-аппа, которой у пользователей нет). Сейчас поддержан только to:{slot_id}; to:{user_id}/to:{username}, deeplink, dedup_key пока не поддержаны. Отличие от серверного host/notify (§7a): тот шлёт только push офлайн-получателю по app-key, а message:notify — bridge из WebView и основной путь для отправки по slot_id (создаёт реальное сообщение). Доступно с Android 2.27.2+1455.
Launch-сессия мини-аппа короткоживущая (30 мин). Контейнер X2Chat работает как token-broker: при истечении токена (host-API вернул 401) или по запросу контейнер сам переиздаёт сессию через настоящую авторизацию пользователя, ре-инжектит свежий window.X2Chat.launch.sessionToken и шлёт событие. Перелогиниваться или перезапускать приложение не нужно — вызовы переживают истечение прозрачно (контейнер делает авто-retry один раз).
// форсировать переиздание проактивно (не дожидаясь 401)
const r = await window.x2.requestCapability('session:refresh', {});
// → { success: true, sessionToken }
// хост шлёт событие после успешного переиздания;
// к этому моменту window.X2Chat.launch.sessionToken уже свежий
window.addEventListener('x2chat:session', (e) => {
if (e.detail.state === 'active') { /* токен обновлён */ }
});
session:refresh обрабатывается в контейнере (не уходит в backend). Backend поддерживает sliding — каждый вызов продлевает ещё живую сессию. Рекомендация: на ответ host-API с 401/expired вызовите session:refresh один раз и повторите запрос. Доступно с Android 2.27.2+1454.
При установке пользователь соглашается с подмножеством available_scopes; в launch-сессию попадают только выданные granted_scopes.
| Scope | Назначение |
|---|---|
profile:read | Профиль пользователя (id, email, username, display_name, avatar, language, theme). |
chat:context | Контекст чата привязки (id, title, type). |
message:send | Черновик сообщения в чат. |
contacts:pick | Выбор контакта → slot_id + display_name собеседника (см. §3.4). |
voice:personas:read | Каталог голосовых персонажей (официальный сценарий звонков). |
voice:persona:set | Выбор голосового персонажа. |
Re-consent. Если вы расширили available_scopes уже установленного приложения — пользователь должен переустановить его, иначе requestCapability(новый_scope) вернёт 403.
Самостоятельное приложение может логинить пользователя без отдельного входа, доверившись X2Chat-сессии:
window.X2Chat.launch.sessionToken и отправляет его на свой бэкенд вместе с window.location.origin (JWT на клиенте не валидируем).origin выбирает дата-центр и делает server-to-server запрос:GET <X2CHAT_HOST_API>/api/v1/mini-apps/host/me
Header: X-X2Chat-Mini-App-Session: <sessionToken>
→ { success, profile: { id, email, username, display_name, avatar_url, language, theme } }
401 — токен невалиден / истёк / чужой ДЦ
403 — не выдан scope profile:read
По стабильному profile.id заведите/найдите своего пользователя и выдайте свою сессию. Токен X2Chat живёт ~5 минут и больше не нужен.
Dual-DC. Токен валиден только на том ДЦ, что его выпустил. Выбирайте Host API по origin: ru.x2chat.com → RU, ix.x2chat.com → IX. Внутренние адреса Host API выдаёт платформа.
Отдельного метода «вложить картинку» нет — используйте превью ссылок (OpenGraph):
og:title, og:description, og:image (абсолютный URL), og:site_name.window.X2Chat.postMessage({
method: 'sendMessageDraft',
payload: { text: 'Подпись\nhttps://ru.x2chat.com/apps/<slug>/s/<token>' }
});
Клиент X2Chat сам анфёрлит ссылку и рисует карточку с картинкой. Превью фетчит устройство каждого получателя (User-Agent X2Chat/LinkPreview, таймаут 5 с, кэш 6 ч) — поэтому страница и картинка должны быть публично доступны. Рекомендуемая картинка: PNG/JPEG, ~1.91:1.
Базовый путь: /api/v1/mini-apps/host. Заголовок сессии: X-X2Chat-Mini-App-Session: <JWT>.
| Метод | Путь | Scope |
|---|---|---|
| GET | /me | profile:read |
| GET | /chat | chat:context |
| POST | /message-draft | message:send |
| POST | /capabilities/request | тело { scope, payload }; scope должен быть в сессии |
Серверный бэкенд приложения может отправить получателю системный push (работает офлайн). Аутентификация — app-key (выдаёт платформа), не launch-сессия.
POST <X2CHAT_HOST_API>/api/v1/mini-apps/host/notify
Headers: X-X2Chat-App-Key: <секрет>
Body: { "to": {"user_id"|"slot_id"}, "push": {"title","body"},
"deeplink"?: "https://… (только https/x2chat)", "dedup_key"?: "…" }
→ { "success": true, "pushed": true, "deduped": false }
dedup_key.401 невалидный ключ · 403 нет права/IP · 429 лимит.host/notify vs message:notify. Этот серверный host/notify шлёт только push офлайн-получателю (app-key, без сообщения в чате). Чтобы из WebView отправить реальное E2E-сообщение получателю по slot_id (пуш придёт штатно) — используйте bridge-capability message:notify (см. §3.5); для slot_id это основной путь.
Приложение можно проксировать под путём на доменах X2Chat: https://ru.x2chat.com/apps/<slug>/ и https://ix.x2chat.com/apps/<slug>/.
fetch('api/...'), src="static/..." — без ведущего /). Абсолютный /api/... ушёл бы на API самого X2Chat.launch_url — со слешем на конце; добавьте редирект /apps/<slug> → /apps/<slug>/.mini_apps не реплицируется между ДЦ — регистрируйтесь на каждом ДЦ со своим launch_url.Каталог мини-аппов в клиенте — сетка иконок. Иконку поставляет само приложение по соглашению (как favicon) — никуда отдельно загружать не нужно.
Положите файл icon.png рядом с вашим launch_url (в той же папке):
launch_url: https://ru.x2chat.com/apps/otc/
иконка: https://ru.x2chat.com/apps/otc/icon.png
launch_url: https://cdn.example.com/app/index.html
иконка: https://cdn.example.com/app/icon.png
X2Chat подтянет её автоматически (icon_url вычисляется из launch_url). Клиент грузит иконку вживую — заменили файл, иконка обновилась без переустановки. Требования: квадрат, ≥ 256×256, PNG. Нет файла (404) → показывается градиент с первой буквой названия. name держите коротким (2 строки под иконкой).
| Симптом | Причина |
|---|---|
| Приложения нет в каталоге | Нет записи на этом ДЦ, либо status ≠ enabled / review_status ≠ approved |
403 на Host API | Scope не выдан при установке (нужен re-consent) |
401 на Host API | Launch-сессия истекла/отозвана, либо запрос ушёл на чужой ДЦ |
| WebView блокирует переход | URL вне allowed_origins |
| Ломаются ассеты/API под путём | Фронт ходит абсолютными путями вместо относительных (§8) |