Покажите QR на странице «Подключить X2Chat». Юзер сканит его в приложении — бот настраивается на ваш webhook одним подтверждением. Ни bot_token, ни secret в QR не попадают.
Show a QR on your "Connect X2Chat" page. The user scans it in the app — the bot is configured to call your webhook with one tap. No bot_token, no secret in the QR.
webhook_url + site_name + expires_at. Никакого секрета.my-site.com». Юзер вводит «Да» (typed confirmation, не tap).secret_token (32 байта random) и делает первый ping на ваш webhook_url с заголовком X-X2Chat-Secret-Token: <secret> и body {"setup_confirm": true, "bot": {...}}.200 OK в течение 5 сек → X2Chat применяет setWebhook(url, secret) и записывает в БД. Если timeout/non-2xx — откат без сохранения (нет «висящих» неработающих интеграций).webhook_url с тем же header'ом для проверки подписи. Сайт сравнивает header с сохранённым secret.webhook_url + site_name + expires_at. No secrets.my-site.com". User types "Yes" (typed confirmation, not a button tap).secret_token (32 random bytes) and makes the first ping to your webhook_url with header X-X2Chat-Secret-Token: <secret> and body {"setup_confirm": true, "bot": {...}}.200 OK within 5 sec → X2Chat applies setWebhook(url, secret) and persists. If timeout/non-2xx — rollback without saving (no dangling broken integrations).webhook_url with the same header for signature verification. Site compares the header with the stored secret.В QR кодируется plain-JSON. Только 5 полей:
QR encodes plain JSON. Only 5 fields:
{
"v": 1,
"type": "x2chat-webhook-setup",
"webhook_url": "https://my-site.com/x2chat-webhook",
"site_name": "MyService",
"expires_at": "2026-05-22T12:05:00Z"
}
| Field | Type | Описание | Description |
|---|---|---|---|
v | integer | Версия формата. Сейчас 1Format version. Currently 1 | |
type | string | "x2chat-webhook-setup" | |
webhook_url | string | HTTPS URL вашего endpoint'а. Не LAN, не raw IP, не localhostHTTPS URL of your endpoint. No LAN, no raw IP, no localhost | |
site_name | string | Короткое название сервиса для UI (≤ 32 символов, printable ASCII)Short service name for UI (≤ 32 chars, printable ASCII) | |
expires_at | string ISO-8601 | Когда QR протухнет. Рекомендуем now+5minWhen the QR expires. Recommended: now+5min |
⚠ Что НЕ должно быть в payload: логотипы, длинные описания, ссылки на политику, lock'и, hashes, secret_token. Лишние поля игнорируются X2Chat'ом, но создают phishing-вектор для других сервисов.
⚠ What MUST NOT be in payload: logos, long descriptions, policy links, locks, hashes, secret_token. Extra fields are ignored by X2Chat but create phishing surface for other parsers.
После confirmation X2Chat делает один HTTP POST на ваш webhook_url:
After confirmation X2Chat makes one HTTP POST to your webhook_url:
POST /your/x2chat-webhook HTTP/1.1
Host: my-site.com
Content-Type: application/json
X-X2Chat-Secret-Token: 9f4e7a2bd9c6f1... // 32-байт hex
X-X2Chat-Setup-Confirm: true
{
"setup_confirm": true,
"bot": {
"id": "d03bff3b-...uuid",
"username": "my_info_bot",
"first_name": "My Info Bot"
},
"owner_user_slot": "XI9W4A",
"issued_at": "2026-05-22T12:00:00Z"
}
X-X2Chat-Secret-Token из header'аbot.id и (если есть) к вашему пользователю200 OK в течение 5 секунд (любой 2xx без body)X-X2Chat-Secret-Token from headerbot.id and (if applicable) your user200 OK within 5 seconds (any 2xx without body)Заполните форму — QR обновляется автоматически. Скопируйте код в свой сайт.
Fill in the form — QR updates automatically. Copy the code to your site.
// 1) Render page with QR
import express from 'express';
import QRCode from 'qrcode';
const app = express();
app.use(express.json());
app.get('/connect-x2chat', async (req, res) => {
const payload = {
v: 1,
type: 'x2chat-webhook-setup',
webhook_url: 'https://my-site.com/x2chat-webhook',
site_name: 'MyService',
expires_at: new Date(Date.now() + 5*60*1000).toISOString(),
};
const qrDataUrl = await QRCode.toDataURL(JSON.stringify(payload), { width: 320 });
res.send(`<h2>Сканируйте в X2Chat:</h2><img src="${qrDataUrl}">`);
});
// 2) Setup confirmation ping
app.post('/x2chat-webhook', (req, res) => {
const secret = req.headers['x-x2chat-secret-token'];
if (req.body?.setup_confirm) {
// First ping — save secret + bot info
saveX2ChatBot({
bot_id: req.body.bot.id,
bot_username: req.body.bot.username,
secret_token: secret,
});
return res.sendStatus(200);
}
// Regular update — verify secret
const stored = lookupSecretByBotId(req.body?.bot?.id);
if (secret !== stored) return res.sendStatus(401);
// process update (message/callback_query/...)
console.log('Update from', req.body);
res.sendStatus(200);
});
app.listen(3000);
# requirements: fastapi, qrcode[pil], pydantic
import qrcode, json, io, base64
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get('/connect-x2chat', response_class=HTMLResponse)
def connect_page():
payload = {
'v': 1,
'type': 'x2chat-webhook-setup',
'webhook_url': 'https://my-site.com/x2chat-webhook',
'site_name': 'MyService',
'expires_at': (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat(),
}
img = qrcode.make(json.dumps(payload))
buf = io.BytesIO(); img.save(buf, format='PNG')
b64 = base64.b64encode(buf.getvalue()).decode()
return f'<h2>Сканируйте в X2Chat:</h2><img src="data:image/png;base64,{b64}">'
bot_secrets: dict = {} # bot_id -> secret
@app.post('/x2chat-webhook')
async def webhook(req: Request):
body = await req.json()
secret = req.headers.get('x-x2chat-secret-token')
if body.get('setup_confirm'):
bot_secrets[body['bot']['id']] = secret
return {'ok': True}
bot_id = body.get('message', {}).get('bot', {}).get('id')
if secret != bot_secrets.get(bot_id):
raise HTTPException(401)
print('Update:', body)
return {'ok': True}
expires_at X2Chat отказывается даже сканировать payloadwebhook_url должен начинаться с https://, иначе ping не пойдёт🚨 Никогда не запрашивайте secret_token напрямую у пользователя. Если ваш сайт показывает форму «введите secret из X2Chat» — это phishing. X2Chat НЕ показывает secret юзеру, он доходит до вашего сайта только через ping.
🚨 Never ask the user for secret_token directly. If your site shows a "paste secret from X2Chat" form — that's phishing. X2Chat does NOT show the secret to the user; it reaches your site only via the ping.
| Code | Reason | Что делать | What to do |
|---|---|---|---|
400 | Invalid payload schema | Проверьте поля QR. Все 5 обязательны, типы строгиеVerify QR fields. All 5 are required, types strict | |
400 | webhook_url not https / LAN / raw IP | Используйте публичный HTTPS URL с домен-именемUse public HTTPS URL with domain name | |
410 | QR expired | Refresh QR на сайтеRefresh QR on the site | |
504 | Setup ping timed out (5s) | Ваш endpoint должен отвечать быстро. Не делайте тяжёлую логику в setup_confirm — только save+200Your endpoint must reply fast. Don't run heavy logic on setup_confirm — just save+200 | |
502 | Setup ping returned non-2xx | Проверьте логи сайта. Endpoint должен возвращать любой 2xxCheck site logs. Endpoint must return any 2xx | |
429 | Rate limit (5 setup attempts/hour) | Подождите час или revoke старые webhook'иWait an hour or revoke old webhooks |
© X2Chat · Bot API docs · bots@x2chat.com
© X2Chat · Bot API docs · bots@x2chat.com