Авторизация в приложении на Black Hole
Black Hole — это закрытый режим сервера приложения: снаружи он недоступен напрямую, единственный вход — через платформенный шлюз Gateway. Когда сотрудник открывает приложение внутри Битрикс24, Gateway проверяет, кто это, и кладёт данные пользователя прямо в заголовки запроса к серверу приложения. Приложению не нужны ни своя форма входа, ни хранение паролей: оно доверяет заголовкам от Gateway, а когда нужны данные портала — обращается к API за этого пользователя. Кто может открыть приложение, задаёт политика доступа — Доступ и режимы.
Gateway сам аутентифицирует пользователя и пробрасывает к приложению уже подписанный запрос. Браузер не видит и не хранит vibe_session_* — токен живёт только между Gateway и сервером приложения. Приложение получает заголовок X-Vibe-Authorization: Bearer vibe_session_<…> и читает данные пользователя через GET /v1/me. Это BFF-паттерн (Backend-for-Frontend) — тот же, что у Cloudflare Access, Google IAP и AWS API Gateway.
Скоуп: vibe:apps (на стороне V1 API), внутри туннеля — без явного скоупа, Gateway сам управляет доступом.
Заголовки запроса · Параметры iframe · Данные пользователя · Примеры кода · Жизненный цикл сессии
Жизненный цикл запроса
Полный обмен — от открытия приложения в Битрикс24 до проксирования запроса на сервер приложения — показан ниже по шагам. Разбор ключевых моментов идёт сразу после схемы.
Битрикс24 placement Backend (vibecode.bitrix24.tech)
│ │
├── POST /v1/bitrix-handler ──────────────►│ exchange OAuth code,
│ (B24 placement triple) │ upsert AppUserToken,
│ │ createAppSession (mint vibe_session_*),
│ │ mint signed __init JWT (TTL 5 min)
│ │
│◄── 302 https://app-XXX/?placement=…&member_id=…[&placement_options=…]&__init=<jwt> ──┤
│
Browser
│
├── GET https://app-XXX/?placement=…&member_id=…[&placement_options=…]&__init=<jwt> ───►│ Gateway
│ │ Priority 0 redeem:
│ │ • verify JWT (HMAC-SHA256, "gateway-init")
│ │ • subdomain binding
│ │ • mark jti used (one-time)
│ │ • store (sub, subdomain) → raw in cache
│ │ • mint _vibe_gw cookie
│ │
│◄────────── 302 https://app-XXX/?placement=…&member_id=…[&placement_options=…] ───────┤
│ Set-Cookie: _vibe_gw=…
│
├── GET https://app-XXX/?placement=…&member_id=…[&placement_options=…] ──────────────►│ Gateway
│ Cookie: _vibe_gw=… │ Priority 1 (cookie):
│ │ • resolve raw from cache
│ │ • strip incoming X-Vibe-Authorization (anti-spoof)
│ │ • inject X-Vibe-Authorization: Bearer vibe_session_…
│ │
│ ├── GET / ─────► App server
│ │ X-Vibe-Authorization: Bearer vibe_session_…
│ │ Cookie: _vibe_gw=… (Gateway-only; не для app)
│ │
│◄────────────────────── HTTP response ───────────────────────────────┤
Ключевая разница от стандартного OAuth:
- Браузер никогда не видит
vibe_session_*— даже в момент первого открытия. Токен передаётся только в подписанном одноразовом коде в URL, который Gateway сразу обменивает и стирает. __init— одноразовый. Повторный обмен того жеjtiотклоняется. Время жизни — 5 минут. На медленных сетях загрузки iframe этого хватает с запасом — если не уложитесь в 5 минут, проверьте журналы Gatewayinit code replay rejected/init code verify failed.- Cookie
_vibe_gwживёт 10 минут — а у placement-приложений (открытых через встройку в Битрикс24) около 40 минут — и продлевается при активности. Не пытайтесь её читать в JS — онаHttpOnly. - Пробуждение из сна: если сервер в
SLEEPING, Gateway обменивает__initдо страницы пробуждения, кладёт исходные данные в кэш и cookie в ответ. После переподключения агента браузер делаетlocation.replaceна чистый URL — кэш ещё не истёк, иX-Vibe-Authorizationпроставляется на первый же GET.
Что приходит в каждый запрос
GET /api/tasks
Host: app-9edac24091ba.vibecode.bitrix24.tech
Cookie: _vibe_gw=<…> ← платформенная cookie, к приложению пробрасывается прозрачно
X-Vibe-Request-Id: req_<16 hex chars>
X-Vibe-User-Id: 42
X-Vibe-Portal-Id: f2342f7a-b1c5-4d50-8a3e-30219c5ae0c1
X-Vibe-User-Name: Иван Иванов
X-Vibe-User-Name-Encoded: %D0%98%D0%B2%D0%B0%D0%BD%20%D0%98%D0%B2%D0%B0%D0%BD%D0%BE%D0%B2
X-Vibe-User-Role: MEMBER
X-Vibe-Authorization: Bearer vibe_session_<43 chars>
Gateway проставляет семь заголовков на каждый проксируемый запрос. X-Vibe-Request-Id приходит на любой запрос, остальные — только когда пользователь аутентифицирован (пройдена авторизация через placement или по Bearer-токену):
| Заголовок | Когда | Значение |
|---|---|---|
X-Vibe-Request-Id |
всегда | Идентификатор запроса для корреляции в журналах. Префикс req_ + 16 hex-символов. |
X-Vibe-User-Id |
аутентифицированный запрос | Числовой ID пользователя в Битрикс24 (например, 42). Совпадает с currentUser.bitrixUserId в ответе /v1/me. |
X-Vibe-Portal-Id |
аутентифицированный запрос | UUID портала во внутренней БД Вайбкод. Не путать с доменом — он отдельно. |
X-Vibe-User-Name |
аутентифицированный запрос | Отображаемое имя пользователя в виде сырых UTF-8 байтов. ⚠️ Большинство HTTP-стеков (Express и др.) читают значения заголовков как latin1 (RFC 7230 §3.2.4), поэтому локализованные имена отображаются нечитаемыми символами (Ðван вместо Иван). Для корректного имени используйте X-Vibe-User-Name-Encoded (ниже). Заголовок сохранён для обратной совместимости. |
X-Vibe-User-Name-Encoded |
аутентифицированный запрос | То же отображаемое имя, в процентном кодировании по RFC 3986 (UTF-8). Декодируется любым стандартным декодером: в JS — decodeURIComponent(req.headers['x-vibe-user-name-encoded']). Рекомендуемый источник имени вместо сырого X-Vibe-User-Name. |
X-Vibe-User-Role |
аутентифицированный запрос | ADMIN или MEMBER — роль текущего пользователя (того, кто открыл приложение) на портале. ADMIN означает право управлять настройками приложения (предикат Битрикс24 user.admin) — в том числе у владельца портала. Это надёжный способ определить, администратор ли текущий пользователь. Признака администратора для произвольного пользователя из GET /v1/users Битрикс24 не отдаёт (см. поле isAdmin в доках сущности users). |
X-Vibe-Authorization |
аутентифицированный запрос | Bearer vibe_session_<…> — токен доступа для серверных вызовов V1 API. |
X-Vibe-Authorization— единственный источник Bearer-токена. Платформенный префиксX-Vibe-выбран, чтобы не конфликтовать с собственнойAuthorization-цепочкой приложения (например, если оно проксирует к своему вышестоящему API). Любой входящийX-Vibe-Authorizationот клиента Gateway срезает до проброса — приложение никогда не получит подделанный токен.X-Vibe-User-Id/-Portal-Id/-User-Name/-User-Role— быстрый способ опознать пользователя без дополнительного вызова/v1/me. Подходят для логов, аналитики и простого ACL. Для полного контекста (scopes,capabilities,tariff) всё равно делайте один вызов/v1/meи кэшируйте его на токен.- Имя пользователя — читайте
X-Vibe-User-Name-Encoded, неX-Vibe-User-Name. СыройX-Vibe-User-Nameнесёт UTF-8 байты, которые HTTP-стеки трактуют как latin1 → кириллица и локализованные имена отображаются нечитаемыми символами. Корректное имя:const name = decodeURIComponent(req.headers['x-vibe-user-name-encoded'] ?? ''). Старый заголовок оставлен как есть, чтобы не ломать приложения, уже использующие обходной путьBuffer.from(name, 'latin1').toString('utf8'). X-Vibe-Request-Id— кладите в свой структурированный лог, чтобы трассировать запрос между Gateway и приложением. При обращении в поддержку Вайбкод присылайте этот ID — он есть и в журналах платформы.Cookie: _vibe_gw— оставляется как есть, но это не токен доступа к V1 API. Это сессионный токен Gateway для быстрой авторизации последующих запросов. Приложению она не нужна и помеченаHttpOnly— JS её даже не увидит.access_token/auth[*]в теле запроса — не приходят. Не пытайтесь их читать.
URL-параметры iframe после placement-редиректа
После POST /v1/bitrix-handler платформа перенаправляет (302) браузер на <appUrl> и добавляет в строку запроса:
| Параметр | Когда | Значение |
|---|---|---|
placement |
всегда | Код placement-а (LEFT_MENU, CRM_DEAL_DETAIL_TAB, IM_TEXTAREA, …). Совпадает с PLACEMENT в POST от B24. |
member_id |
всегда | Идентификатор портала Bitrix24 (тот же member_id, что в B24 OAuth). |
placement_options |
когда B24 присылает PLACEMENT_OPTIONS — типично для DETAIL_TAB / DETAIL_TOOLBAR / LIST_MENU и других placement'ов, где есть контекст сущности | JSON-строка с контекстом сущности — например {"ID":"42"} для CRM_DEAL_DETAIL_TAB. Сюда B24 кладёт ID текущей карточки (сделки/лида/контакта/компании). |
__init |
первый запрос | Одноразовый JWT для Gateway, обмениваемый на _vibe_gw cookie. После обмена — исчезает (замена на чистый URL). |
Заметка по именованию: B24 шлёт это поле как PLACEMENT_OPTIONS (в верхнем регистре) в POST к /v1/bitrix-handler. Мы пробрасываем его под именем placement_options в нижнем регистре в URL iframe. Это одно и то же поле — ищите в спецификации B24 по имени в верхнем регистре.
Как читать placement_options — забирайте из URLSearchParams и парсите JSON:
const params = new URLSearchParams(window.location.search)
const placement = params.get('placement') // "CRM_DEAL_DETAIL_TAB"
const options = JSON.parse(params.get('placement_options') || '{}')
const entityId = options.ID // "42" — текущая карточка
Не используйте BX24.placement.info() — SDK не видит контекст при перенаправлении через платформенный обработчик и вернёт {placement:null, options:null}. Всё, что вам нужно про placement и его параметры, уже лежит в URL.
Получение данных пользователя (`GET /v1/me`)
X-Vibe-Authorization несёт только токен доступа, не данные пользователя. Чтобы узнать пользователя, портал, скоупы и capabilities — один раз вызываем /v1/me на сервере приложения и кэшируем в собственной сессии:
GET https://vibecode.bitrix24.tech/v1/me
X-Api-Key: <ключ-вашего-приложения>
Authorization: Bearer vibe_session_<…> ← забрали из X-Vibe-Authorization
200 OK
{
"success": true,
"data": {
"type": "oauth_app",
"portal": "company.bitrix24.ru",
"scopes": ["crm", "tasks", "imbot", ...],
"currentUser": {
"bitrixUserId": "42",
"_note": "B24 numeric user id of the OAuth-authorized end user (from Bearer session)."
},
"capabilities": { "servers": { "create": { "available": true, "reason": "TRIAL_ACTIVE" }, ... }, ... },
"tariff": { ... },
"app": { "title": "My App", "id": "<uuid>" }
}
}
Показаны характерные поля. В ответе также есть api (список доступных эндпоинтов и сущностей), oauth, oauthTutorial, deployment, infra, feedback и другие — модель использует их, чтобы построить запросы без чтения остальной документации.
Идентификатор пользователя — data.currentUser.bitrixUserId (строка с числовым ID Битрикс24). Не userId, не user.id, не верхнеуровневое поле — именно currentUser.bitrixUserId. Совпадает с заголовком X-Vibe-User-Id (см. секцию «Что приходит в каждый запрос»), который Gateway уже проставил в запрос. Для простой идентификации заголовка достаточно. /v1/me нужен, когда требуются scopes, capabilities, tariff или информация о приложении.
Без Authorization: Bearer тот же /v1/me отвечает с тем же набором полей, только currentUser будет null. Это рабочий режим: используется для серверной инициализации приложения до OAuth — портал, скоупы и capabilities уже видны. Не пытайтесь распознавать «нет сессии» по 401 — его не будет, проверяйте currentUser !== null.
Кэш — 24 часа TTL. После 401 на V1 — отправляйте пользователя обратно на placement (см. ниже).
Примеры кода
Node.js / Express
import express from 'express'
const app = express()
const VIBE_API = 'https://vibecode.bitrix24.tech'
const VIBE_APP_KEY = process.env.VIBE_APP_KEY // ваш vibe_app_<…> ключ
app.use(async (req, _res, next) => {
// Gateway уже всё проверил — но на всякий случай подстрахуемся
const bearer = req.headers['x-vibe-authorization']?.toString().replace(/^Bearer /, '')
if (!bearer) return next() // anonymous (PUBLIC policy) — кэшируем без identity
// Кэшируем identity in-process per token (одно обращение к /v1/me на сессию).
// /v1/me возвращает { success, data: { currentUser, portal, scopes, ... } } —
// разворачиваем envelope, чтобы дальше работать с самими полями.
let me = identityCache.get(bearer)
if (!me) {
const res = await fetch(`${VIBE_API}/v1/me`, {
headers: { 'X-Api-Key': VIBE_APP_KEY, Authorization: `Bearer ${bearer}` },
})
if (!res.ok) return next() // 401 → показать экран повторной авторизации
const json = await res.json()
me = json.data
identityCache.set(bearer, me)
}
req.user = me // me.currentUser?.bitrixUserId, me.portal, me.scopes, me.capabilities
req.bearer = bearer
next()
})
app.get('/api/tasks', async (req, res) => {
// currentUser может быть null, если запрос без Bearer пришёл (PUBLIC-политика).
// Альтернатива — прочитать заголовок X-Vibe-User-Id (Gateway уже его проставил).
const userId = req.user?.currentUser?.bitrixUserId ?? req.headers['x-vibe-user-id']
const r = await fetch(`${VIBE_API}/v1/tasks?filter[RESPONSIBLE_ID]=${userId}`, {
headers: { 'X-Api-Key': VIBE_APP_KEY, Authorization: `Bearer ${req.bearer}` },
})
res.json(await r.json())
})
Python / FastAPI
from fastapi import FastAPI, Request, HTTPException
import httpx
VIBE_API = "https://vibecode.bitrix24.tech"
VIBE_APP_KEY = os.environ["VIBE_APP_KEY"]
app = FastAPI()
identity_cache: dict[str, dict] = {}
async def resolve_identity(request: Request):
raw = request.headers.get("x-vibe-authorization", "")
bearer = raw.removeprefix("Bearer ")
if not bearer:
return None
if bearer in identity_cache:
return identity_cache[bearer]
async with httpx.AsyncClient() as c:
r = await c.get(
f"{VIBE_API}/v1/me",
headers={"X-Api-Key": VIBE_APP_KEY, "Authorization": f"Bearer {bearer}"},
)
if r.status_code != 200:
return None
# /v1/me возвращает {"success": True, "data": {...}} — кладём в кэш именно data.
me = r.json()["data"]
identity_cache[bearer] = me
return me
@app.get("/api/tasks")
async def tasks(request: Request):
me = await resolve_identity(request)
if not me:
raise HTTPException(401, "Re-authentication required")
# currentUser может быть None при запросе без Bearer (PUBLIC-политика); откат на заголовок Gateway.
user_id = (me.get("currentUser") or {}).get("bitrixUserId") or request.headers.get("x-vibe-user-id")
async with httpx.AsyncClient() as c:
r = await c.get(
f"{VIBE_API}/v1/tasks",
params={"filter[RESPONSIBLE_ID]": user_id},
headers={"X-Api-Key": VIBE_APP_KEY,
"Authorization": request.headers["x-vibe-authorization"]},
)
return r.json()
Go / net-http
package main
import (
"encoding/json"
"net/http"
"os"
"strings"
"sync"
)
const vibeAPI = "https://vibecode.bitrix24.tech"
var vibeAppKey = os.Getenv("VIBE_APP_KEY")
var identityCache sync.Map // bearer string -> *Identity
// CurrentUser совпадает по полям с объектом data.currentUser в ответе /v1/me.
// Когда запрос пришёл без Bearer (PUBLIC-политика) — data.currentUser приходит как null,
// и поле CurrentUser в раскодированной структуре остаётся пустым (BitrixUserID == "").
type CurrentUser struct {
BitrixUserID string `json:"bitrixUserId"`
}
type Identity struct {
Portal string `json:"portal"` // домен портала, например "company.bitrix24.ru"
Scopes []string `json:"scopes"`
CurrentUser *CurrentUser `json:"currentUser"` // nil без Bearer
}
type meEnvelope struct {
Success bool `json:"success"`
Data Identity `json:"data"`
}
func resolveIdentity(r *http.Request) (*Identity, string, error) {
raw := strings.TrimPrefix(r.Header.Get("X-Vibe-Authorization"), "Bearer ")
if raw == "" {
return nil, "", nil
}
if v, ok := identityCache.Load(raw); ok {
return v.(*Identity), raw, nil
}
req, _ := http.NewRequest("GET", vibeAPI+"/v1/me", nil)
req.Header.Set("X-Api-Key", vibeAppKey)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, "", nil
}
var env meEnvelope
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
return nil, "", err
}
identityCache.Store(raw, &env.Data)
return &env.Data, raw, nil
}
// Пример использования: r.User.CurrentUser.BitrixUserID — числовой ID пользователя Битрикс24.
// Альтернатива — заголовок r.Header.Get("X-Vibe-User-Id"), Gateway проставил его на входе.
Жизненный цикл сессии
Одновременно действуют два срока, и путать их не нужно:
- Токен
vibe_session_*живёт 24 часа (AppSession.expiresAt). Продление не делается — после истечения пользователь заново открывает placement. - Сессия Gateway (
_vibe_gw) живёт 10 минут, а у placement-сессий (приложение открыто через встройку в Битрикс24) — около 40 минут. Она продлевается при активности: запрос к серверу приложения во второй половине жизни сессии (для обычной — когда до истечения осталось меньше 5 минут, для placement — примерно после 20-й минуты) получает свежую cookie. Пока приложение получает запросы, сессия держится и заголовокX-Vibe-Authorizationпроставляется автоматически.
Отсюда следствие, которое стоит учитывать: 401 может прийти раньше, чем истекут 24 часа токена — если сервер приложения не получает запросов дольше жизни Gateway-сессии (для placement-приложений это около 40 минут, для остальных — 10). Например, пользователь долго заполняет форму целиком на стороне браузера (поиск сотрудников, перетаскивание, привязка отделов), не обращаясь к серверу. По истечении сессии следующий запрос приходит к приложению уже без заголовка X-Vibe-Authorization, и вызов к V1 возвращает 401, хотя сам токен ещё действителен. Для placement-приложений расширенное окно (~40 минут) покрывает типичные паузы в 10–15 минут — 401 после короткого простоя больше не возникает.
Как с этим работать:
- На
401от V1 — попросите пользователя заново открыть приложение из меню Битрикс24. Важно: это должно быть именно повторное открытие из меню (оно ре-POST-ит placement, и Gateway выдаёт свежую сессию), а не возврат на уже открытую вкладку — повторный показ кэшированной вкладки идёт обычным GET без нового POST и сессию не восстановит. Серверный редирект здесь не поможет: из iframe он выйти не может. Самостоятельно обновлять токен не нужно. - Для длинных форм и редакторов — сохраняйте изменения по частям или держите сессию активной периодическим лёгким запросом к своему серверу, пока пользователь работает (любой запрос к приложению проходит через Gateway и продлевает сессию). Учтите: браузер тротлит фоновые вкладки, поэтому keepalive надёжен только пока вкладка приложения активна.
- Серверная фоновая работа без пользователя (крон, интеграция) — отдельный случай:
vibe_session_*привязана к конкретному пользователю и на сервере без его браузера не продлевается. Если нужен долгоживущий серверный доступ ОТ ИМЕНИ ВЛАДЕЛЬЦА сервера, используйте api-bearer-токен и его продление —POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh(требует включённого администратором платформыaccessTokensEnabled). Такой токен действует от владельца, а не от текущего пользователя, — не подменяйте им пер-юзерные вызовы V1.
// На 401 от V1 сессия Gateway истекла. Серверный редирект не выйдет из iframe —
// верните 401 своему SPA и предложите пользователю заново открыть приложение из
// меню Битрикс24 (это ре-POST-ит placement и выдаст свежую сессию). Токен сам не обновляйте.
if (vibeResponse.status === 401) {
return res.status(401).json({ error: 'session_expired', reopen: true })
}
Что не делать
- ❌
sessionStorage.setItem('vibe_token', …)— токена в браузере физически нет, и не должно быть. OAuth 2.0 Security BCP § 4.1 явно запрещает хранение access-токенов вsessionStorage/localStorage. Не пытайтесь воссоздать его на стороне клиента. - ❌ Возвращать токен в JSON-ответе SPA.
X-Vibe-Authorizationживёт исключительно между Gateway и сервером приложения. Раскрытие в ответе браузеру уничтожает всё преимущество BFF-паттерна (XSS → утечка токена). - ❌ Читать
_vibe_gwcookie руками. Cookie помеченаHttpOnly— JS даже не увидит её. Если хочется session ID для логирования — генерируйте свой. - ❌ Использовать
AuthorizationвместоX-Vibe-Authorization. Платформенный префикс выбран специально, чтобы приложение могло пользоваться своей собственнойAuthorization-цепочкой (например, для проксирования к своему вышестоящему сервису). - ❌ Хранить
vibe_session_*где-либо за пределами одной HTTP-сессии. Это токен доступа, который действует 24 часа и охватывает весь V1 API в скоупе приложения. Любая утечка — компрометация. - ❌ Пытаться декодировать
__initили_vibe_gw. Это HMAC-подписанные JWT с одним получателем — Gateway. Содержимое внутреннее, в будущем поменяется.
Аналогия для понимания
Модель для понимания: Cloudflare Access / Google IAP / AWS API Gateway. Прокси (Gateway) делает всю аутентификацию и кладёт данные пользователя в заголовки на входе. Приложение читает заголовки и доверяет им, потому что:
- Заголовок может прийти только от Gateway (всё остальное срезается).
- Туннель защищён mTLS / WebSocket на уровне инфраструктуры.
- Если приложению нужна возможность — оно делает серверный вызов на платформенное API, неся токен как Bearer.
Идиоматическое чтение — req.headers['x-vibe-authorization'] (или эквивалент вашего фреймворка). Идиоматический ответ на 401 от V1 — перенаправление пользователя на placement, не самостоятельное обновление токена.
Входящие запросы извне: вебхуки и события Битрикс24
По умолчанию приложение на Black Hole — приватное: Gateway аутентифицирует каждый запрос (цепочка выше). Внешний вызов без платформенной cookie / Bearer — например исходящий вебхук Битрикс24 или POST от обработчика события — получает 401 BH_LOGIN_REQUIRED. Формат ответа зависит от запроса: для Content-Type: application/json или пути /v1/… — JSON-ошибка, для запроса из браузера — HTML-страница входа. Поэтому «настроил вебхук на адрес приложения → приходит 401» — ожидаемое поведение приватного режима, а не сбой.
Чтобы принимать запросы извне, переведите сервер в публичный режим:
curl -X PATCH https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/access-policy \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accessPolicy": "PUBLIC" }'
В режиме PUBLIC Gateway пробрасывает внешний запрос прямо в приложение с заголовком X-Vibe-Public: true — аутентификация не обязательна. Доступ открыт всем, кто знает адрес субдомена, поэтому проверяйте входящие данные на стороне приложения (для событий Битрикс24 — поле auth.application_token в теле запроса). Если же у посетителя уже есть сессия, Gateway дополнительно прокидывает заголовки идентичности X-Vibe-User-* (и X-Vibe-Authorization, когда у пользователя есть токен приложения) — так же, как на приватных политиках. Анонимный запрос приходит без них. Опознан ли посетитель — определяйте по наличию X-Vibe-User-Id, а не X-Vibe-Authorization: последний несёт только токен доступа для /v1 и законно пуст у гостевых ссылок и у пользователей без токена приложения, даже когда идентичность известна. Подробнее о режимах — Политика доступа.
Таймаут — 30 секунд. Gateway ждёт ответа приложения до 30 секунд. Внутренний 504 при этом перехватывается самим шлюзом и до вызывающего в исходном виде не доходит: обычный запрос получает HTML-страницу запуска или пробуждения приложения со статусом 503, поэтому распознать таймаут по телу ответа нельзя. Для долгих задач (анализ, генерация, импорт) не держите соединение открытым: ответьте сразу (202 + идентификатор задачи), выполняйте работу в фоне, а вызывающий опрашивает статус отдельным запросом. Синхронный POST дольше 30 секунд через публичный субдомен не дойдёт до конца.
Спящий сервер теряет первый запрос. Если сервер в SLEEPING, входящий запрос его будит, но вызывающий в этот момент получает HTML-страницу пробуждения (со статусом 503), а не ответ приложения — то есть полезная нагрузка первого вебхука/события до приложения не доходит. Битрикс24 события отправляет без гарантии повторной доставки. Поэтому для надёжного приёма событий отключите авто-сон (PATCH /v1/infra/servers/:id/sleep со значением «никогда») — иначе первое событие после засыпания будет потеряно.
Можно ли направить событие Битрикс24 (`event.bind`) на субдомен приложения
Да. Битрикс24 event.bind принимает в качестве обработчика произвольный HTTPS-URL, поэтому адрес субдомена Black Hole (https://app-<hex>.vibecode.bitrix24.tech/<путь>) подходит — это разблокирует событийную архитектуру (событие в Битрикс24 → действие в приложении) без опроса каждые N минут. Условия:
- HTTPS обязателен — субдомены Black Hole всегда по HTTPS, условие выполнено.
- Публичный режим —
accessPolicy: PUBLIC(см. выше), иначе POST от Битрикс24 упрётся в401. - Сервер не должен засыпать — иначе первое событие потеряется (см. выше). Отключите авто-сон.
- Ответ за 30 секунд — обработчик должен подтвердить приём быстро, тяжёлую обработку выносите в фон.
event.bindвызывается через REST API самого Битрикс24 в контексте приложения. События доступны на коммерческих тарифах Битрикс24. В теле запроса Битрикс24 присылаетauth.application_token— проверяйте его.
Если держать сервер постоянно активным нельзя, надёжнее остаётся опрос (polling) на стороне приложения.
Placement открывается с экраном входа вместо приложения
Симптом: сотрудник открывает вкладку с placement в Битрикс24, но в iframe показывается экран входа, а не приложение. Либо приложение пускает только владельца ключа, а другим сотрудникам из списка доступа по-прежнему предлагает войти. Две причины конфигурации.
Обработчик placement указывает на субдомен, а не на платформу
Битрикс24 должен открывать placement через обработчик платформы https://vibecode.bitrix24.tech/v1/bitrix-handler, а не напрямую на субдомен приложения (https://app-<hex>.vibecode.bitrix24.tech). Только через обработчик платформа создаёт сессию vibe_session_* и одноразовый код для Gateway (см. «Жизненный цикл запроса» выше). Если placement привязан напрямую к субдомену, Битрикс24 открывает iframe в обход платформы — сессия не создаётся, и Gateway показывает экран входа.
Адрес обработчика задаёт платформа, указывать субдомен приложения вручную не нужно. Если placement уже привязан к субдомену — переопубликуйте приложение: публикация заново привязывает обработчик к платформенному адресу.
Список доступа применяется только при политике `NAMED_USERS`
В режиме OWNER_ONLY доступ есть только у владельца ключа — записи списка доступа, добавленные через POST /access, не применяются. Чтобы пустить конкретных сотрудников портала, переключите политику на NAMED_USERS и добавьте пользователей:
curl -X PATCH https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/access-policy \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accessPolicy": "NAMED_USERS" }'
Подробнее о режимах и списке доступа — Политика доступа.
После обоих исправлений сотрудники из списка проходят в iframe автоматически. Ручной вход на субдомене в отдельной вкладке не требуется — если вы пробовали этот обходной путь, после исправления обработчика он не нужен.
Self-hosted приложение (не Black Hole)
Заголовок X-Vibe-Authorization проставляется только за Gateway, который стоит перед Black Hole-серверами. Если файлы приложения лежат на вашем собственном сервере, заголовок не подставляется — приложение само хранит токен сессии (свой BFF) и само вызывает GET /v1/me.
Как получить токен сессии текущего пользователя в placement (iframe):
- Приложение Вайбкод с
appUrlна вашем сервере. Placement приходит на обработчик Вайбкод, который перенаправляет на<appUrl>/?code=<одноразовый код>&placement=…&member_id=…. Ваш сервер обменивает код:POST /v1/oauth/token { app_key, code, redirect_uri }(значениеredirect_uri— ровно настроенныйappUrl). В браузер попадает только одноразовый код, не токен. - Собственное приложение Битрикс24 со своим обработчиком. Placement приходит на ваш сервер с токеном пользователя (
AUTH_ID). Обменяйте его сервер-к-серверу:POST /v1/oauth/placement-session { app_key, access_token, member_id, domain }.
В обоих случаях ответ — { access_token: "vibe_session_…", user, expires_in }. Дальше приложение работает как BFF: хранит vibe_session_* на своей стороне, читает личность через GET /v1/me с Authorization: Bearer <vibe_session_*> и не отдаёт токен в браузер. Возможность встроить свою страницу в iframe — заголовки X-Frame-Options и CSP frame-ancestors — обеспечиваете вы, платформа за неё не отвечает. Стандартный GET /v1/oauth/authorize в iframe не открывается: страница согласия Битрикс24 запрещает встраивание. Открывайте его только в отдельной вкладке.