Для AI-агентов: markdown этой страницы — /docs-content/infra/app-runtime.md индекс документации — /llms.txt
Авторизация в приложении на 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, либо значение с префиксом, если ID в портале у посетителя нет (см. «Виды значений» ниже). |
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 означает право управлять настройками приложения — в том числе у владельца портала. Это надёжный способ определить, администратор ли текущий пользователь. Для произвольного сотрудника из GET /v1/users признак администратора недоступен — он приходит только в профиле текущего сотрудника. Важно: одно исключение — если приложение открыли из каталога Битрикс24, а у сотрудника ещё нет учётной записи Вайбкод, роль приходит MEMBER независимо от его прав на портале — брать её неоткуда, роль хранится в учётной записи. Не завязывайте на этот заголовок необратимые решения. Для проверки прав запросите профиль текущего сотрудника и прочитайте isAdmin. |
X-Vibe-Authorization |
аутентифицированный запрос | Bearer vibe_session_<…> — токен доступа для серверных вызовов V1 API. |
Виды значений `X-Vibe-User-Id`
| Вид значения | Кто это |
|---|---|
только цифры (42) |
пользователь портала Битрикс24 |
net_<id> |
пользователь Битрикс24 Нетворк, не состоящий в портале (политика «Аутентифицированные») |
share:<…> |
анонимный посетитель по гостевой ссылке |
vibe:<…> |
пользователь платформы Вайбкод, у которого ID в портале определить не удалось |
Прежде чем передавать значение в метод REST Битрикс24 — убедитесь, что оно состоит только из цифр. Значение с префиксом означает «у этого посетителя нет ID в портале», и подставлять его в USER_ID, DIALOG_ID или RESPONSIBLE_ID нельзя: Битрикс24 приводит строку к числу, и нечисловой идентификатор может превратиться в ID постороннего сотрудника. Заголовок может и вовсе отсутствовать — у анонимного посетителя на политике PUBLIC.
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и кэшируйте его на токен.- Значения относятся к порталу, которому принадлежит приложение — независимо от того, открыл посетитель приложение плиткой из портала или по прямой ссылке. Участник нескольких порталов Битрикс24 приходит со своим номером сотрудника, именем и ролью именно в этом портале. Если карточку сотрудника прочитать не удалось, имя приходит из его учётной записи на платформе Вайбкод — пустым заголовок не приходит.
- Имя пользователя — читайте
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.
Открытие сущностей Битрикс24 из приложения
Приложение живёт на своём субдомене внутри iframe, а карточки сделок, задач и остальных сущностей — на портале. Поддерживаемый способ открыть карточку: увести верхнее окно на её адрес по клику пользователя.
// portal — домен, который сервер приложения получил из GET /v1/me.
// Только внутри обработчика клика. Навигации верхнего окна из вложенного
// iframe нужна transient user activation: на загрузке страницы её нет, а после
// await она может истечь или быть потреблённой другим API — навигация не сработает.
button.addEventListener('click', () => {
window.top.location.href = `https://${portal}/crm/deal/details/${dealId}/`
})
Домен для такой ссылки берите только из доверенного источника, и он один: поле portal в ответе GET /v1/me. Это ответ платформы, привязанный к сессии вашего приложения, — подставить в него чужой домен снаружи нельзя.
document.location.ancestorOrigins[0] для этой задачи не подходит, хотя список и заполняет сам браузер. Его первый элемент — origin непосредственного родителя, а это не обязательно портал: во вложенном фрейме родителем окажется промежуточный сайт, и ссылка уведёт верхнее окно к нему. Сверять ancestorOrigins с доменом из /v1/me тоже смысла нет — раз доверенный ответ уже получен, домен берётся сразу из него.
Строку запроса собственного приложения доверенным источником не считайте. Её формирует тот, кто открыл ссылку, а не платформа, и подставить туда можно что угодно — в том числе правдоподобный домен. Навигация верхнего окна уводит пользователя туда, куда указывает домен, поэтому приложение, доверяющее домену из своего же URL, уводит пользователя на чужой сайт с адреса, которому тот доверял.
Штатный placement-редирект домен портала не передаёт — в нём только placement, member_id, placement_options и __init (у self-hosted вместо __init приходит code). Но если домен в строке запроса всё-таки оказался, это не признак того, что его прислала платформа: он мог попасть туда как угодно, и проверять его всё равно нужно по доверенному источнику.
Это навигация, а не слайдер поверх приложения. Верхнее окно уходит на страницу Битрикс24, приложение при этом размонтируется. Возврат — навигацией назад средствами Битрикс24.
Примеры адресов:
| Сущность | Адрес |
|---|---|
| Сделка | /crm/deal/details/<id>/ |
| Лид | /crm/lead/details/<id>/ |
| Контакт | /crm/contact/details/<id>/ |
| Компания | /crm/company/details/<id>/ |
| Предложение | /crm/type/7/details/<id>/ |
| Счёт | /crm/type/31/details/<id>/ |
| Элемент смарт-процесса | /crm/type/<entityTypeId>/details/<id>/ |
| Задача | /company/personal/user/<responsibleId>/tasks/task/view/<id>/ |
Список рекомендательный, а не исчерпывающий: платформа адрес перехода не ограничивает — вести можно на любую страницу портала, которая открывается сама по себе.
Нативный BX24 JS SDK не работает, если приложение открыто через обработчик Вайбкод. BX24.init не завершается — и вместе с ним недоступны BX24.openPath и BX24.openSlider. Причина та же, что и у BX24.placement.info() выше: после перенаправления обработчика фрейм открыт на другом источнике и без контекста Битрикс24, которого SDK ждёт от родительского окна. Поэтому — навигация верхнего окна.
Так открываются все приложения Вайбкод — и на Black Hole, и с appUrl на вашем сервере: placement в обоих случаях приходит на обработчик платформы, а тот перенаправляет фрейм на адрес приложения. Исключение — собственное приложение Битрикс24 со своим обработчиком (см. Self-hosted приложение): placement приходит сразу на ваш источник, перенаправления нет, контекст SDK на месте — там BX24.init работает штатно и этот раздел не нужен.
Авто-высота iframe
Внутри Битрикс24 приложение открыто во встроенном iframe, и его высоту задаёт Битрикс24, а не контент: если страница выше окна, низ обрезается, собственного скролла у iframe нет. Чтобы платформа растила iframe под контент, приложение само сообщает свою высоту — постит сообщение родительскому окну (window.parent, не window.top). Платформенная обёртка ловит это сообщение и релеит высоту в Битрикс24, а тот подстраивает высоту плейсмента.
Обёртка включается у приложения отдельно. Пока она выключена, платформа открывает встройку прежним способом — перенаправлением на адрес приложения, и высота не подстраивается. Чтобы платформа отдавала обёртку, включите опцию «Авто-высота iframe» в карточке приложения либо передайте placementResizeEnabled: true в PATCH /v1/apps/:id.
Опция — необходимое условие, но не единственное: сама возможность раскатывается по порталам постепенно. Если опция включена, а высота встройки всё равно не подстраивается — обратитесь в поддержку, чтобы функцию открыли вашему порталу.
Если приложение отдаёт собственную директиву frame-ancestors — добавьте в неё источник платформы. Под обёрткой приложение открыто во вложенном iframe на источнике платформы, то есть у приложения появляется новый источник-предок, а браузер сверяет frame-ancestors по всей цепочке предков. Приложение, разрешившее только домен портала, браузер не откроет, и средствами платформы это не исправить. Добавьте https://vibecode.bitrix24.tech в свою директиву frame-ancestors. Приложения без собственного заголовка Content-Security-Policy правок не требуют.
// Пересчитывайте при изменении контента — надёжнее всего через ResizeObserver на корне приложения.
// targetOrigin обязан быть '*' (или origin обёртки) — почему не домен портала, см. ниже.
const height = document.documentElement.scrollHeight
window.parent.postMessage({ type: 'vibe:resize', height }, '*')
targetOrigin — '*' или origin обёртки, но не домен портала. Браузер сверяет targetOrigin отправителя с origin окна-получателя ещё до доставки. Непосредственный родитель приложения под обёрткой — сама обёртка, а не портал, поэтому захардкоженный домен портала в targetOrigin заставит браузер отбросить сообщение раньше, чем обёртка его увидит, и со стороны платформы это уже не лечится. '*' безопасен: обёртка всё равно проверяет origin отправителя, прежде чем менять высоту.
Что принимается. Поле type — vibe:resize или vibe:setHeight. Высота — число пикселей в поле height, нечисловое значение отбрасывается. Обёртка не измеряет ваш контент за вас — высоту всегда несёт приложение, поэтому сообщение без числовой высоты не делает ничего.
Ещё раз про ancestorOrigins[0]. Под обёрткой предупреждение из раздела «Открытие сущностей Битрикс24 из приложения» становится буквальным: document.location.ancestorOrigins[0] — это origin обёртки (vibecode.bitrix24.tech), а не домен портала. Домен по-прежнему берите только из ответа GET /v1/me (поле portal) — единственного доверенного источника.
Получение данных пользователя (`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 (см. секцию «Что приходит в каждый запрос») — но только когда тот несёт число: у посетителя без ID в портале заголовок приходит с префиксом (net_ / share: / vibe:). Для простой идентификации заголовка достаточно, для вызовов Битрикс24 — проверяйте, что значение состоит из цифр. /v1/me нужен, когда требуются scopes, capabilities, tariff или информация о приложении.
Без Authorization: Bearer тот же /v1/me отвечает с тем же набором полей, только currentUser будет null. Это рабочий режим: используется для серверной инициализации приложения до OAuth — портал, скоупы и capabilities уже видны. Не пытайтесь распознавать «нет сессии» по 401 — его не будет, проверяйте currentUser !== null.
Кэш — 24 часа TTL. После 401 на V1 — отправляйте пользователя обратно на placement (см. ниже).
Приложение как сервис: собственный ключ (X-Api-Key)
Всё выше — это модель per-user: приложение внутри Битрикс24 работает от имени текущего пользователя, форвардит X-Vibe-Authorization, и /v1/* читается его правами. Для неё нужен OAuth-app-ключ vibe_app_ (создаётся в разделе «Ключи авторизации») плюс проброшенная Bearer-сессия — личный ключ vibe_api_ Bearer-сессию нести и биндить placement не может.
Ключ сервера и ключ приложения — разные вещи. Сервер, на котором работает приложение, может принадлежать личному ключу vibe_api_: Gateway всё равно проставит X-Vibe-Authorization, потому что сессию выписал placement, а не ключ сервера. А вот в самом запросе к /v1/* пара заголовков обязана быть согласованной — X-Api-Key должен быть ключом того же приложения, которое выписало сессию в Authorization: Bearer, иначе ответ будет 403 SESSION_APP_MISMATCH.
Но приложение бывает и сервисом: сервер, бэкенд или дашборд, который сам дергает /v1/* без пользовательской сессии (например, отчёт по всей компании). Тогда оно аутентифицируется собственным личным ключом vibe_api_ в заголовке X-Api-Key. Ключ создаёт пользователь в разделе «API-ключи» (/keys), передаёт приложению через параметр env при деплое (например VIBE_API_KEY), а приложение читает его из окружения и шлёт как X-Api-Key. Авто-генерируемого «ключа сервера» не существует.
Как выбрать поток. Показываете данные, зависящие от текущего пользователя, — используйте per-user-поток выше с форвардом X-Vibe-Authorization. Один общий X-Api-Key читается как владелец ключа, а не текущий пользователь, и все посетители увидят данные владельца. Личный ключ обращается к Битрикс24 от имени владельца, поэтому для сервисного дашборда держите политику доступа сервера в OWNER_ONLY (см. Политика доступа) и выбирайте владельца ключа осознанно. Для отчёта, который только читает данные, выпускайте ключ в режиме READONLY с минимальными правами.
Примеры кода
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 уже его проставил).
// Заголовок может нести префиксованную идентичность (net_ / share: / vibe:) —
// для вызовов Битрикс24 годится только числовое значение.
const rawId = req.headers['x-vibe-user-id']
const userId = req.user?.currentUser?.bitrixUserId ?? (/^\d+$/.test(rawId ?? '') ? rawId : null)
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.
# Заголовок может нести префиксованную идентичность (net_ / share: / vibe:) —
# для вызовов Битрикс24 годится только числовое значение.
raw_id = request.headers.get("x-vibe-user-id") or ""
user_id = (me.get("currentUser") or {}).get("bitrixUserId") or (raw_id if raw_id.isdigit() else None)
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"). Он может нести префиксованную
// идентичность (net_ / share: / vibe:) — для вызовов Битрикс24 проверьте, что это только цифры.
Жизненный цикл сессии
Одновременно действуют два срока, и путать их не нужно:
- Токен
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 он выйти не может. Самостоятельно обновлять токен не нужно. - Не пишите «откройте из меню» как единственный текст ошибки. Совет верен только для истёкшей сессии. У
401есть вторая причина: пользователь открывает приложение ПЕРВЫЙ раз и ещё не выдал ему доступ — тогда открытие из меню ничего не изменит, нужна авторизация приложения или один вход через встройку. Различить их на стороне приложения нечем, поэтому формулируйте нейтрально («нужно заново войти или выдать приложению доступ»). Первый вход из каталога платформа теперь перехватывает сама и показывает экран с кнопкой «Авторизовать приложение» — до вашего кода такой запуск не доходит. - Для длинных форм и редакторов — сохраняйте изменения по частям или держите сессию активной периодическим лёгким запросом к своему серверу, пока пользователь работает (любой запрос к приложению проходит через Gateway и продлевает сессию). Учтите: браузер тротлит фоновые вкладки, поэтому keepalive надёжен только пока вкладка приложения активна.
- Серверная фоновая работа без пользователя (крон, интеграция) — отдельный случай:
vibe_session_*привязана к конкретному пользователю и на сервере без его браузера не продлевается. Если нужен долгоживущий серверный доступ ОТ ИМЕНИ ВЛАДЕЛЬЦА сервера, используйте api-bearer-токен и его продление —POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh(требует включённого на платформе раздела токенов доступа — Доступность). Такой токен действует от владельца, а не от текущего пользователя, — не подменяйте им пер-юзерные вызовы 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. Формат ответа зависит от запроса: API-клиент — с заголовком Accept: application/json, Authorization: Bearer, X-Api-Key, X-Requested-With: XMLHttpRequest или Sec-Fetch-Dest: empty, а также по пути /v1/… и /api/… — получает JSON-ошибку, обычный запрос из браузера — HTML-страницу входа со статусом 200. Поэтому «настроил вебхук на адрес приложения → приходит 401» — ожидаемое поведение приватного режима, а не сбой.
На этом отказе признак «не браузер» уже, чем у состояний туннеля ниже: метод запроса здесь не учитывается. Вебхук, который приходит голым POST без единого из перечисленных заголовков, получит страницу входа со статусом 200, а не JSON. Чтобы отказ приходил разбираемым, добавьте вызывающему заголовок Accept: application/json.
Чтобы принимать запросы извне, переведите сервер в публичный режим:
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. Вызывающему, который не браузер (вебхук, интеграция, fetch), вместо страницы приходит JSON-ошибка с кодом BH_APP_STARTING и заголовком Retry-After — по нему такой ответ отличим от ответа самого приложения. Для долгих задач (анализ, генерация, импорт) не держите соединение открытым: ответьте сразу (202 + идентификатор задачи), выполняйте работу в фоне, а вызывающий опрашивает статус отдельным запросом. Синхронный POST дольше 30 секунд через публичный субдомен не дойдёт до конца.
Спящий сервер и первый запрос. Если сервер спит и запрос попадает под условия автоматического пробуждения — например публичный режим и метод POST, — платформа начинает подъём. Что увидит вызывающий, зависит от того, кто он:
- Браузер получает HTML-страницу пробуждения со статусом
503. Она сама опрашивает сервер и перезагружается, когда приложение поднялось. - Не-браузер получает JSON-ошибку со статусом
503, кодомBH_SERVER_WAKING(илиBH_TUNNEL_CONNECTING, если сервер уже поднят и переподключается агент) и заголовкомRetry-After: повторите запрос через указанное число секунд.
Кто считается не-браузером. Достаточно любого одного признака: заголовок Authorization: Bearer, заголовок X-Api-Key, Accept: application/json, X-Requested-With: XMLHttpRequest, Sec-Fetch-Dest: empty, путь запроса начинается на /v1/ — либо метод запроса POST, PUT, PATCH или DELETE. Последнее и покрывает вебхук Битрикс24, который приходит без единого из перечисленных заголовков. Обратно: запрос, у которого Sec-Fetch-Dest равен document, iframe или frame, считается переходом в браузере и получает страницу, а не JSON, — даже если это POST формы.
Полный набор кодов, которые отдаёт сам шлюз. Все они приходят в общем конверте ошибки { "success": false, "error": { "code", "message" } }.
| HTTP | Код | Что произошло | Retry-After |
|---|---|---|---|
| 503 | BH_SERVER_WAKING |
Сервер спал, платформа его поднимает | есть |
| 503 | BH_TUNNEL_CONNECTING |
Сервер поднят, агент переподключается | есть |
| 503 | BH_APP_STARTING |
Туннель открыт, приложение ещё не слушает свой порт | есть |
| 503 | BH_APP_NOT_DEPLOYED |
Туннель открыт, но выкладки на этот сервер платформа не видела | нет |
| 502 | BH_TUNNEL_DISCONNECTED |
Туннель к серверу оборван | есть |
| 500 | BH_SERVER_ERROR |
Сервер в состоянии ошибки — повтор не поможет, нужен ремонт | нет |
| 404 | BH_SERVER_NOT_FOUND |
За этим адресом нет сервера: удалён либо адрес набран неверно | нет |
| 402 | BH_WAKE_BLOCKED |
Пробуждение заблокировано — исчерпан баланс либо нужна подписка | нет |
| 503 | BH_AUTH_UNAVAILABLE |
Проверить доступ сейчас не удалось — временный сбой на стороне платформы, это не отказ в доступе | есть |
| 403 | BH_ACCESS_DENIED |
Вызывающий опознан, но политика доступа сервера его не пускает | нет |
| 401 | BH_LOGIN_REQUIRED |
Приватный режим: вызывающий не прошёл проверку доступа | нет |
Коды без Retry-After повторять по таймеру бессмысленно: состояние само не изменится.
Короткий разрыв туннеля на уже работающем сервере шлюз старается пережить незаметно: запрос удерживается до ~15 секунд, и если туннель за это время возвращается, запрос доставляется в приложение и вызывающий получает настоящий ответ приложения, а не ошибку.
Холодный старт в это окно не укладывается: поднятие машины занимает от минуты, поэтому первый запрос к по-настоящему спящему серверу до приложения не доходит и повторить его должен вызывающий. Битрикс24 события отправляет без гарантии повторной доставки, поэтому для событийных сценариев выберите одно из двух. Отключите авто-сон (PATCH /v1/infra/servers/:id/sleep со значением «никогда») — тогда обработчик всегда онлайн. Либо перейдите на офлайн-события Битрикс24 — тогда сервер может спать (см. ниже).
Можно ли направить событие Битрикс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— проверяйте его.
Если сервер должен засыпать: офлайн-события Битрикс24
Битрикс24 умеет не вызывать обработчик, а складывать изменения в очередь на своей стороне — приложение забирает их само, когда проснулось. Это штатный механизм ровно для случая «приложение временно недоступно», и он снимает требование «сервер не должен спать».
- Подписка —
event.bindсevent_type=offlineи без параметраhandler. - Чтение —
event.offline.get. Сclear=0метод не удаляет записи, а помечает пачку идентификаторомprocess_id. После успешной обработки подтвердите еёevent.offline.clear, сбойные записи отметьтеevent.offline.error. Зарезервированная пачка хранится до 30 дней. - Без опроса по таймеру — подпишитесь обычным (онлайн) способом на
onOfflineEvent: это сигнал «в очереди появились записи», данных он не несёт. Учтите, что сам этот сигнал приходит на обработчик и на спящем сервере теряется так же, как любое онлайн-событие, — как страховка оставьте периодическое чтение очереди. - Против петель — если приложение само меняет объекты в Битрикс24, передавайте
auth_connectorи при подписке, и в вызовах изменения: тогда собственные правки приложения в его очередь не попадут. - Очередь хранит не историю, а текущее состояние объекта: тысяча правок одной сделки оставит в ней одну запись.
- Часть возможностей (подтверждение обработки,
auth_connector) зависит от тарифа Битрикс24 — проверьте доступность методомfeature.get.
Карточка ссылки на приложение
Ссылку на приложение в режиме PUBLIC мессенджеры и чат Битрикс24 разворачивают в карточку с названием, описанием и картинкой. Собирает её сам Gateway: приложение запроса не получает, спящий сервер не просыпается.
Данные берутся из карточки приложения в каталоге — название, описание и иконка. Названия в каталоге нет — подставляется имя сервера. Описания нет нигде — карточка остаётся с одним названием. Иконки нет — картинка платформы.
На любой другой политике доступа карточки нет: сборщик превью получает нейтральную страницу без названия, описания и картинки. Так же отвечает адрес, за которым нет сервера. Подробнее о режимах — Политика доступа.
Gateway запрещает HTTP-кеширование и карточки, и нейтральной страницы заголовком Cache-Control: no-store: каждый новый запрос заново проверяет текущую политику доступа. Это не управляет кешем на стороне мессенджера после получения карточки.
Уже показанную карточку отозвать нельзя. Принимающая сторона кеширует её у себя, поэтому переключение политики с PUBLIC на приватную закрывает только новые запросы: в отправленных раньше сообщениях карточка продолжает показываться столько, сколько её хранит мессенджер. Не включайте PUBLIC, если название или описание приложения не должны попасть наружу.
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 уже привязан к субдомену — привяжите место заново вызовом Привязать место: привязка регистрирует платформенный адрес обработчика. Переданный адрес субдомена платформа заменяет сама и отмечает замену полем handlerRewritten в ответе.
Список доступа применяется только при политике `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 запрещает встраивание. Открывайте его только в отдельной вкладке.