# Внешний API приложения

Внешний API открывает HTTP-контур приложения тем, у кого есть ключ: внешняя система или другое приложение отправляет запрос платформе, платформа передаёт его приложению и возвращает ответ как есть.

**Базовый URL:** `https://vibecode.bitrix24.tech/v1` | **Авторизация:** `X-Api-Key` | **Ключ:** ключ внешнего API приложения

[Когда это нужно](#когда-это-нужно) | [Что включить](#что-включить-на-стороне-приложения) | [Ключ](#ключ) | [Адрес и методы](#адрес-и-методы) | [Что получает приложение](#что-получает-приложение) | [Что возвращается вызывающему](#что-возвращается-вызывающему) | [Примеры](#примеры) | [Приложение, которое зовут автоматизации](#приложение-которое-зовут-автоматизации) | [Ограничения](#ограничения) | [Ошибки](#ошибки)

## Когда это нужно

Канал решает две задачи.

**Внешняя система вызывает приложение.** Интегратор или сервис заказчика получает ключ внешнего API и обращается к маршрутам приложения напрямую: платформа доставляет запрос приложению и возвращает ответ вызывающему как есть.

**Приложение вызывает другое приложение.** Двум приложениям одного портала не нужно договариваться о сетевом доступе и придумывать собственную аутентификацию: вызывающее приложение отправляет запрос по адресу платформы с ключом вызываемого.

Во внутренний контур портала канал не ходит: это ровно проброс до вашего кода. Запросы к данным Битрикс24 по-прежнему идут через [Entity API](/docs/entity-api).

## Что включить на стороне приложения

Канал работает при трёх условиях.

1. **Переключатель «Внешний API» включён в карточке приложения.** Он стоит в карточке приложения на платформе и принимает значения «Включён» и «Выключен». Управлять им может тот, кто управляет приложением.
2. **У приложения есть сервер.** Без сервера включить переключатель нельзя, а вызов возвращает `409 APP_API_NO_SERVER`.
3. **Сервер не засыпает.** Автоматический сон отключён, тариф сервера не вытесняемый, окна пробуждения не заданы. Иначе переключатель не включится, а вызов вернёт `409 APP_API_NOT_ALWAYS_ON`. Как отключить авто-сон — [Настроить авто-сон](/docs/infra/lifecycle/sleep). У galaxy-приложения этот метод отвечает `400 GALAXY_APP_USE_GALAXY_ROUTE` — таймер сна ему через API не задаётся, отключите авто-сон в кабинете, в карточке сервера приложения.

**Важно: включённый переключатель публикует весь HTTP-контур приложения держателям ключа.** Отбора маршрутов на стороне платформы нет — держатель ключа может обратиться к любому пути приложения. Аутентификацию и разграничение доступа своих маршрутов приложение делает само: платформа оставляет для этого свободным заголовок `Authorization` и передаёт его приложению без изменений.

Выключить переключатель можно всегда и без условий. Выключение сразу закрывает канал для всех выданных ключей.

## Ключ

Внешний API принимает **только ключ, выпущенный для этого приложения**. Личный API-ключ и ключ авторизации приложения сюда не подходят: они получают `403 APP_API_NOT_GRANTED`. Обратное тоже верно — ключ внешнего API не открывает остальные адреса платформы, попытка вызвать ими что-то ещё возвращает `403 APP_API_KEY_OUT_OF_SCOPE`.

**Где взять.** Карточка приложения на платформе, блок «Внешний API», кнопка «Получить ключ внешнего API». Секрет показывается один раз — платформа не отдаёт его повторно.

**Кто может выпустить.** Владелец приложения либо администратор портала. Владельцем выпущенного ключа в обоих случаях становится владелец приложения — он же платит за вызовы.

**Сколько ключей.** До 10 действующих ключей на приложение. Одиннадцатый выпуск возвращает `409 APP_API_KEY_LIMIT` с полями `limit` и `used`. Отзыв ключа освобождает место сразу. Срока жизни у ключа нет — он действует до отзыва, до выключения переключателя или до смены владельца приложения.

**Смена владельца приложения отзывает все ключи внешнего API.** Передали приложение другому человеку — выпустите ключи заново и обновите их в роботах и вызывающих приложениях.

Ключ передаётся одним из двух заголовков, оба равноправны:

```
X-Api-Key: YOUR_APP_EXTERNAL_API_KEY
Authorization: Bearer YOUR_APP_EXTERNAL_API_KEY
```

Вторая форма нужна клиентам, которые умеют только `Authorization`. Если приложению нужен собственный `Authorization`, передавайте ключ платформы через `X-Api-Key` — тогда заголовок `Authorization` доедет до приложения нетронутым.

## Адрес и методы

```
METHOD https://vibecode.bitrix24.tech/v1/applications/:applicationId/api/<путь в приложении>
```

Принимаются `GET`, `POST`, `PUT`, `PATCH`, `DELETE` и `HEAD`. Метод доезжает до приложения тем же, каким пришёл.

| Параметр | Тип | Обяз. | Описание |
|----------|-----|:-----:|----------|
| `applicationId` (path) | string | да | Идентификатор приложения. Список: [`GET /v1/applications`](/docs/applications/list) |
| `<путь в приложении>` (path) | string | нет | Всё, что идёт после `/api/`, приложение получает как свой путь запроса. Пустой путь означает корень приложения. Предел длины — 2048 символов |
| строка запроса | string | нет | Передаётся приложению дословно, платформа её не разбирает |

**Что отбивается в пути.** Путь проверяется и в исходном виде, и в однократно раскодированном, поэтому обход через процентное кодирование не проходит. Ответ `400 APP_API_BAD_PATH` дают:

- сегмент `.` или `..` в любом месте, в том числе с параметром сегмента после точки с запятой
- путь, начинающийся с `//` или с `/@`
- обратный слэш
- абсолютный адрес вместо пути
- управляющие символы
- битое процентное кодирование
- символы вне набора, пригодного для пути, — их надо кодировать

Точки внутри сегмента законны: `/files/report..v2` проезжает.

**Что отбивается в строке запроса.** Действует один набор допустимых символов по RFC 3986. Внутри значения параметра законны `..`, `//` и `;`, а обратный слэш, пробел и всё за пределами набора обязаны приезжать в процентном кодировании. Иначе — тот же `400 APP_API_BAD_PATH`.

## Что получает приложение

Приложение получает исходный метод, путь после `/api/`, строку запроса и тело запроса без изменений.

К ним платформа добавляет заголовки, которые приложение не может получить ни от кого другого — входящие заголовки с префиксом `X-Vibe-` срезаются, поэтому подделать их вызывающий не может.

| Заголовок | Что несёт |
|-----------|-----------|
| `X-Vibe-Request-Id` | Идентификатор вызова. Тот же идентификатор платформа пишет в свой журнал — называйте его в обращении в поддержку |
| `X-Vibe-Caller-Kind` | Вид вызывающего: `external-api` — вызов через этот канал, `contract` — вызов метода по контракту решения, у долгого метода он описан на странице [Отправить результат долгого метода](/docs/applications/solution-call-result). Это и есть признак, по которому приложение отличает внешний вызов от открытия в интерфейсе Битрикс24 |
| `X-Vibe-Caller-Portal-Id` | Портал, которому принадлежит ключ |
| `X-Vibe-Caller-Key-Id` | Идентификатор ключа, которым сделан вызов. Годится для журналирования и для разграничения доступа внутри приложения |

**Важно: заголовков `X-Vibe-User-Id`, `X-Vibe-User-Name`, `X-Vibe-User-Role` и `X-Vibe-Authorization` на этом пути нет.** Они появляются только тогда, когда приложение открывает человек через интерфейс Битрикс24 и у него есть сессия — [Что приходит в приложение](/docs/infra/app-runtime). Внешний вызов сессии не несёт, поэтому пользовательская личность приложению недоступна: известен портал и ключ, но не сотрудник. Код, написанный по странице «Что приходит в приложение», на внешнем вызове увидит анонимного посетителя — читайте `X-Vibe-Caller-Kind` и расходитесь по ветвям.

**Что срезается из запроса.** Ключ платформы (`X-Api-Key`), `Cookie`, служебные заголовки соединения, `Host`, `Content-Length` и семейства заголовков доверия к прокси целиком: `X-Forwarded`, `X-Original`, `X-Rewrite`, `CF`, `Fastly`. Точечно срезаются имена, из которых стандартные библиотеки читают адрес клиента: `Forwarded`, `X-Real-Ip`, `Client-Ip`, `X-Client-Ip`, `True-Client-Ip`, `X-Cluster-Client-Ip`, `Forwarded-For`, `Appengine-User-Ip`, `X-Appengine-User-Ip`. Значение любого из них у внешнего вызова задаёт сам вызывающий, поэтому приложение его не получает и доверять адресу клиента на этом пути не может.

`Authorization` срезается по **значению**, а не по имени: платформа убирает его только тогда, когда там лежит её собственный ключ. Собственная схема приложения — `Authorization: Bearer eyJ…`, `Basic`, любая другая — доезжает без изменений.

## Что возвращается вызывающему

Статус и тело ответа приложения передаются вызывающему как есть. Заголовки ответа тоже, кроме `Set-Cookie`, `Content-Length`, служебных заголовков соединения и заголовков с префиксом `X-Vibe-` — этот префикс на обоих направлениях принадлежит платформе.

Ответы `3xx` передаются как есть — платформа по ним не переходит, решение принимает вызывающий.

**Отказ платформы отличается от ответа приложения по заголовку `X-Vibecode-Proxy-Error`.** Он стоит на каждом ответе, который построила платформа, и снимается ровно тогда, когда ответ пришёл от приложения. Приложение выставить его не может — из ответа приложения этот заголовок срезается.

Тело отказа платформы — конверт V1:

```json
{
  "success": false,
  "error": {
    "code": "APP_API_NOT_ALWAYS_ON",
    "message": "Application API is not reachable"
  }
}
```

Поэтому разбирать ответ надо в таком порядке: сначала `X-Vibecode-Proxy-Error`, потом статус. Статус `503` с этим заголовком — отказ платформы, статус `503` без него — ответ самого приложения.

Ответ без этого заголовка и без конверта V1 — например HTML-страница с кодом `400` на запрос с двумя заголовками `Content-Length` — пришёл от промежуточного узла на пути к платформе, а не от неё и не от приложения.

Заголовок `X-Vibe-Request-Id` на ответе вызывающему стоит, когда вызов дошёл до туннеля приложения: на ответе приложения и на отказах, которые вернул сам туннель. На отказах до туннеля — ключ, переключатель, сервер, путь, размер тела, — на `503 APP_API_TIMEOUT` и при насыщении канала идентификатора нет: в обращении в поддержку называйте `applicationId` и время вызова.

| Поле | Тип | Описание |
|------|-----|---------|
| `success` | boolean | Всегда `false` — конверт приходит только на отказе платформы |
| `error.code` | string | Код отказа из таблицы [Ошибки](#ошибки) |
| `error.message` | string | Английское пояснение. Разбирайте `error.code`, не текст |

## Примеры

Оба примера показывают ключ внешнего API. Второй оси авторизации у этого адреса нет: личный ключ и ключ авторизации приложения возвращают `403 APP_API_NOT_GRANTED`, потому что не привязаны к приложению.

### curl — ключ внешнего API

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/applications/7f3a1c40-0a2e-4b5d-9c11-2f8e6d3b0a55/api/score" \
  -H "X-Api-Key: YOUR_APP_EXTERNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dealId": 741, "amount": 125000}'
```

### JavaScript — ключ внешнего API

```javascript
const res = await fetch(
  'https://vibecode.bitrix24.tech/v1/applications/7f3a1c40-0a2e-4b5d-9c11-2f8e6d3b0a55/api/score',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_EXTERNAL_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ dealId: 741, amount: 125000 }),
  },
);

if (res.headers.get('x-vibecode-proxy-error') === '1') {
  const { error } = await res.json();
  throw new Error(`платформа отказала: ${error.code}`);
}

const data = await res.json();
```

## Приложение, которое зовут автоматизации

Автоматизации — частый вызывающий, и у них свои привычки. Приложение, рассчитанное на автоматизации, придерживается трёх правил.

**Принимайте и `application/x-www-form-urlencoded`, и JSON.** Автоматизация, отправляющая HTTP-запрос, складывает значения полями формы и JSON не собирает. Приложение, которое читает только JSON, на таком вызове получит пустое тело. Разбирайте оба типа содержимого и сводите их к одной структуре.

**Отвечайте плоским JSON.** Автоматизация кладёт в переменные бизнес-процесса значения верхнего уровня, вложенные объекты и массивы ей недоступны. Ответ вида `{"score": 82, "verdict": "approve"}` разбирается автоматизацией целиком, а `{"result": {"score": 82}}` — нет.

**Отдавайте `GET /openapi.json` со схемой своих маршрутов.** Схема, выложенная приложением, доступна снаружи тем же каналом — по адресу `…/api/openapi.json`. Так вызывающая сторона — человек, другое приложение или AI-агент — узнаёт набор маршрутов и формы тел, не читая ваш код.

## Вызов из другого приложения

Вызывающему приложению нужен ключ **вызываемого**. Положите его в переменные окружения при развёртывании — [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) — и читайте оттуда:

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/applications/${process.env.PARTNER_APP_ID}/api/score`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.PARTNER_APP_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ dealId: 741 }),
  },
);
```

На стороне вызываемого приложения такой запрос отличается от робота значением `X-Vibe-Caller-Key-Id` — заведите отдельный ключ под каждого вызывающего, и приложение сможет их различать.

## Ограничения

| Ограничение | Значение |
|-------------|----------|
| Тело запроса | 4 МиБ. Больше — `413 APP_API_PAYLOAD_TOO_LARGE` |
| Тело ответа приложения | 4 МиБ. Больше — `502 APP_API_RESPONSE_TOO_LARGE` без `Retry-After`: повтор такой ответ не исправит |
| Время ответа приложения | 25 секунд на ответ приложения. Дольше — `503 APP_API_UNAVAILABLE` с `Retry-After`. Общий предел вызова — 30 секунд, за ним `503 APP_API_TIMEOUT` |
| Частота вызовов | 120 запросов в минуту на ключ. Точное значение — в заголовке `x-ratelimit-limit` (потолок делится на реплики). Больше — `429 APP_API_RATE_LIMITED` |
| Одновременные вызовы | Канал ограничивает число вызовов, идущих разом. При насыщении — `503 APP_API_UNAVAILABLE` с `Retry-After` |
| Длина пути | 2048 символов |
| Действующих ключей на приложение | 10 |

Долгую работу выносите за пределы вызова: принимайте задачу, отвечайте роботу сразу и досылайте результат отдельным способом. Двадцать пять секунд — это предел канала, а не рекомендация.

## Пример ответа

Ответ приложения передаётся как есть, поэтому его форму задаёт само приложение. Ответ приложения, написанного по конвенции для роботов:

```json
{
  "score": 82,
  "verdict": "approve",
  "checkedAt": "2026-09-12T08:41:03Z"
}
```

Признак успеха — статус ответа и отсутствие заголовка `X-Vibecode-Proxy-Error`, а не поле `success`: обёртки платформы на успешном ответе нет.

## Пример ответа при ошибке

409 — переключатель внешнего API выключен:

```json
{
  "success": false,
  "error": {
    "code": "APP_API_NOT_ENABLED",
    "message": "Application API is not reachable"
  }
}
```

Текст `message` у отказов допуска один и тот же — `Application API is not reachable` — для `APP_API_NOT_GRANTED`, `APP_API_NOT_ENABLED`, `APP_API_NO_SERVER`, `APP_API_NOT_ALWAYS_ON` и `APP_API_UNAVAILABLE` по серверу. Различайте их по `error.code`.

## Ошибки

Каждый ответ из этой таблицы несёт заголовок `X-Vibecode-Proxy-Error: 1`.

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `APP_API_BAD_PATH` | Путь или строка запроса не прошли проверку |
| 401 | `MISSING_API_KEY` | Заголовок с ключом не передан |
| 401 | `INVALID_API_KEY` | Ключ не существует или отозван |
| 402 | `ACCOUNT_FROZEN` | Счёт владельца приложения заморожен |
| 403 | `APP_API_NOT_GRANTED` | Ключ выпущен для другого приложения или не является ключом внешнего API |
| 403 | `APP_API_KEY_OUT_OF_SCOPE` | Ключом внешнего API вызван другой адрес платформы |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ переведён в режим чтения, а вызов изменяет данные |
| 404 | `APP_API_APP_NOT_FOUND` | Приложения с таким `applicationId` нет или оно удалено |
| 409 | `APP_API_NOT_ENABLED` | Внешний API приложения выключен |
| 409 | `APP_API_NO_SERVER` | У приложения нет сервера |
| 409 | `APP_API_NOT_ALWAYS_ON` | Сервер приложения засыпает — автосон, вытесняемый тариф или заданные окна пробуждения |
| 413 | `APP_API_PAYLOAD_TOO_LARGE` | Тело запроса больше 4 МиБ |
| 429 | `APP_API_RATE_LIMITED` | Превышена частота вызовов на ключ |
| 429 | `QUOTA_EXCEEDED` | Исчерпана квота вызовов владельца приложения |
| 502 | `APP_API_BAD_ENVELOPE` | Приложение ответило вне контракта — например статусом из диапазона `1xx` |
| 502 | `APP_API_RESPONSE_TOO_LARGE` | Ответ приложения больше 4 МиБ. `Retry-After` не приходит |
| 503 | `APP_API_UNAVAILABLE` | Сервер приложения не запущен, приложение не отвечает, или канал насыщен. Приходит с `Retry-After` в секундах |
| 503 | `APP_API_TIMEOUT` | Ответ не пришёл за 30 секунд — общий предел вызова. Приходит с `Retry-After` в секундах |

Полный список общих ошибок API — [Ошибки](/docs/errors).

Ошибки выдачи ключа приходят на карточке приложения, а не на этом адресе: `409 APP_API_KEY_LIMIT` — достигнут предел действующих ключей, `409 APP_API_KEY_ISSUE_CONFLICT` — владелец приложения сменился, пока ключ выпускался.

## Известные особенности

- **Повторять имеет смысл только `503` и `429`.** У них приходит `Retry-After` со сроком в секундах. `502` повтором не лечится: он означает, что ответ приложения не укладывается в контракт канала. **Статуса `504` этот адрес не отдаёт вовсе:** медленное приложение приходит вызывающему как `503` — и когда платформа перестаёт ждать ответ на двадцать пятой секунде (`APP_API_UNAVAILABLE`), и когда вызов упирается в общий предел в тридцать секунд (`APP_API_TIMEOUT`). Увидели `504` на этом адресе — он пришёл не от канала, а от промежуточного узла на пути к платформе. Таймаут не означает, что работа не выполнена: приложение могло довести её до конца после того, как канал перестал ждать, поэтому слепой повтор неидемпотентного вызова задваивает её.
- **Причину `503` различает код, а не статус.** `APP_API_TIMEOUT` означает ровно одно — приложение не ответило за общий предел вызова, и лечится это выносом долгой работы за пределы вызова. `APP_API_UNAVAILABLE` собирает под собой молчащий туннель, недоступный сервер и насыщенный канал: их вызывающему не различить, и остаётся повтор через `Retry-After`. Разобрать причину можно по обращению в поддержку: с идентификатором из `X-Vibe-Request-Id`, если он на ответе есть, иначе — по `applicationId` и времени вызова.
- **Один шумный вызывающий отбирает канал у остальных.** Доля одного портала ограничена, но эта доля — предел, а не резерв. Когда канал занят соседями, отказ `503` получает и тот, кто своей доли не выбрал.
- **Платит владелец вызываемого приложения.** Вызовы расходуют его квоту, а всегда включённый сервер стоит денег независимо от числа вызовов. Вызывающая сторона за канал не платит.
- **Путь приложения в журнал платформы не попадает.** Платформа пишет его длину, но не значение — секрет в пути не станет достоянием журнала. Это не разрешение класть туда секреты: путь видят и вызывающий, и промежуточные узлы.

## Смотрите также

- [Витрина приложений](/docs/applications)
- [Карточка приложения](/docs/applications/get)
- [Что приходит в приложение](/docs/infra/app-runtime)
- [Отправить результат долгого метода](/docs/applications/solution-call-result)
- [Настроить авто-сон](/docs/infra/lifecycle/sleep)
- [Ключи и авторизация](/docs/keys-auth)
- [Ошибки](/docs/errors)
