
# Applications

A Vibecode OAuth application is a record in the catalog of your Bitrix24 account, through which your code is embedded into the interface: CRM cards, the left menu, chats, and other placements. The application lifecycle runs through four steps: create → deploy code → set the URL → publish. Before publishing, only the author sees the application, its `appUrl` is empty, and the `placements` list is empty — this is a normal state, not an error.

**Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** `X-Api-Key` | **Scope for publishing:** `placement`

[Quick start](#quick-start) | [Which key to choose](#which-key-to-choose) | [Application states](#application-states) | [Full cycle](#full-cycle) | [Full example](#full-example) | [Replacing and merging applications](#replacing-and-merging-applications) | [Placements](#placements) | [Known specifics](#known-specifics) | [Windows / PowerShell and UTF-8](#windows-powershell-and-utf-8) | [Error codes](#error-codes) | [Endpoint reference](#endpoint-reference)

## Quick start

Request the applications registered with your personal key in the Bitrix24 account. A successful response confirms that the key works, and the `data` array shows the applications created so far and their `catalogStatus`:

```bash
curl "https://vibecode.bitrix24.com/v1/apps" \
  -H "X-Api-Key: YOUR_API_KEY"
```

Response fields are on the [List applications](/docs/apps/list) page.

## Which key to choose

This section works with two types of keys.

**A personal API key `vibe_api_…`** is bound to one user and one Bitrix24 account and carries its own webhook for that account. Requests run on behalf of the key owner with a single header, `X-Api-Key: vibe_api_…`, and no session token is needed. Use this key to manage application records: create, read, update, and delete. Creating one registers a real local OAuth application on the account, and you can create several such applications under a single personal key.

**An authorization key `vibe_app_…`** is the key of the OAuth application itself. For it to act on behalf of a specific user, the header `Authorization: Bearer <session_token>` is required alongside `X-Api-Key: vibe_app_…` — the platform uses it to look up that user's personal OAuth token. This is the model for applications that act on behalf of different users of the account where the application is registered.

**Placements and publishing.** Publishing can be started with either key, but the placement binding itself is performed by the application's OAuth token — so the application must be authorized on the account via OAuth once, otherwise publishing returns `NO_USER_TOKEN`. For an application that opens inside a Bitrix24 account and acts on behalf of the already signed-in user, only the OAuth application model `vibe_app_…` works — a personal key is not enough.

**How many applications you can create.** The number of keys per Bitrix24 account user is limited — 100 by default. Applications consume the same counter as personal keys, and there is no separate limit for applications. When a limit is reached, creation returns `409 KEY_LIMIT_REACHED`: without `error.details.scope` it is the user limit, while `scope: "portal"` means the platform's per-account cap, which a Bitrix24 account administrator cannot raise. Details — [How many keys you can create](/docs/keys-auth#how-many-keys-you-can-create).

A detailed description of key types, formats, and obtaining `session_token` — [Keys and authorization](/docs/keys-auth).

---

## Application states

| Status | Meaning | Visible in catalog |
|--------|------------|------------------|
| `PRIVATE` | Default state right after creation. Placements are not bound to the account | Author only |
| `PUBLISHED` | Application published, placements bound to the account | All account employees |
| `UNPUBLISHED` | Unpublished, placements unbound, but the catalog record is kept | Everyone, inactive |

Transitions: `PRIVATE → PUBLISHED → UNPUBLISHED → PUBLISHED` — the last arrow is republishing. Unpublishing moves the application to `UNPUBLISHED`, not back to `PRIVATE`: catalog metadata is kept for republishing.

The catalog status is returned in the `catalogStatus` field — one of `PRIVATE` / `PUBLISHED` / `UNPUBLISHED`. The publication date, if the application was ever published, is in `publishedAt`. Determine the status by `catalogStatus`, not by the `placements` array: an empty array does not distinguish `PRIVATE` from `UNPUBLISHED`, and an unpublished application may keep its previously bound codes.

### Why `appUrl: null` and empty `placements` before publishing

A common question: the application is created, the code is deployed, `GET /v1/me` shows `capabilities.apps.publish.available: true`, but [application data](/docs/apps/get) returns `appUrl: null` and an empty `placements` array, and the application is not in the catalog. This is expected while the application is in the `PRIVATE` status:

- `appUrl` is empty until you set it via [updating the application](/docs/apps/update) or pass it in the publish body.
- `placements` is empty because placements are bound to the account only at the moment of publishing.
- `catalogStatus` is `PRIVATE` and `publishedAt` is `null` until the application is published.
- The application appears in the catalog only after [publishing](/docs/apps/publish).

### `appUrl` and `handlerUrl` are different addresses

`handlerUrl` is set by the platform and cannot be changed — OAuth callbacks and placement opening go through it. `appUrl` is your address on Black Hole — the platform redirects there when a placement is opened. You set this address yourself.

---

## Full cycle

### Step 1. Create the application

[Creation](/docs/apps/create) registers an OAuth application on the account and returns the key **once** — in the `rawKey` field of the response. Save it immediately: the key is not shown again. For subsequent publishing, `placement` is **required** in the scope set.

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/apps" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Sales dashboard",
    "scopes": ["crm", "user", "placement"],
    "appUrl": "https://app-abc12345.vibecode.bitrix24.com"
  }'
```

### Step 2. Deploy the code and set the URL

Deploy the application to a Black Hole server ([Deploy API](/docs/infra/deploy)) and make sure `appUrl` points to its address. If the address was not set at creation, set it via [updating](/docs/apps/update):

```bash
curl -X PATCH "https://vibecode.bitrix24.com/v1/apps/APP_ID" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "appUrl": "https://app-abc12345.vibecode.bitrix24.com" }'
```

### Step 3. Publish

[Publishing](/docs/apps/publish) moves the application to `PUBLISHED`, binds placements to the account, and makes the application visible to all employees. Before publishing, the application must be authorized on the account via OAuth, and the key's scope set must include `placement`.

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/apps/APP_ID/publish" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "placements": ["CRM_DEAL_DETAIL_TAB"] }'
```

The reverse path — unpublishing and deletion — is in [Replacing and merging applications](#replacing-and-merging-applications).

---

## Full example

The script takes an application record from creation to deletion: it creates the application, checks its status, sets the `appUrl` address and deletes the application. Publishing is not part of the script: before it, the application is authorized once in the Bitrix24 account via OAuth, and a person does that step in the browser.

```javascript
const BASE = 'https://vibecode.bitrix24.com/v1'
const headers = {
  'X-Api-Key': 'YOUR_API_KEY',
  'Content-Type': 'application/json',
}

const created = await fetch(`${BASE}/apps`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ title: 'Sales dashboard', scopes: ['crm', 'user', 'placement'] }),
}).then((res) => res.json())
if (!created.success) throw new Error(`${created.error.code}: ${created.error.message}`)
const appId = created.data.id
console.log('1. Created:', appId, created.data.catalogStatus)

const app = await fetch(`${BASE}/apps/${appId}`, { headers }).then((res) => res.json())
console.log('2. Status:', app.data.catalogStatus, 'appUrl:', app.data.appUrl, 'placements:', app.data.placements)

const updated = await fetch(`${BASE}/apps/${appId}`, {
  method: 'PATCH',
  headers,
  body: JSON.stringify({ appUrl: 'https://app-abc12345.vibecode.bitrix24.com/sales' }),
}).then((res) => res.json())
console.log('3. New address:', updated.data.appUrl)

const removed = await fetch(`${BASE}/apps/${appId}`, { method: 'DELETE', headers })
console.log('4. Deleted:', removed.status)
```

Output on a test Bitrix24 account:

```
1. Created: f5153dfb-f48f-4c4f-a07f-1c50c3829424 PRIVATE
2. Status: PRIVATE appUrl: null placements: []
3. New address: https://app-abc12345.vibecode.bitrix24.com/sales
4. Deleted: 204
```

The application's authorization key in `rawKey` is returned only in the creation response — save it before the next step. Once the application is deleted, that key stops working.

---

## Replacing and merging applications

An application is taken out of service with two operations: [unpublishing](/docs/apps/unpublish) removes it from the account's interface, and [deletion](/docs/apps/delete) hides the application from the catalog and the account handler, and revokes its authorization key. There is no need to unbind placements one by one beforehand — unpublishing removes them.

### Take an application out of service

1. **Unpublish** — `POST /v1/apps/:id/unpublish`. The application moves to `UNPUBLISHED`, placements are unbound from the account, and the catalog record is preserved. The operation does not guarantee that the account confirmed the removal — see [Unpublish](/docs/apps/unpublish) for details. The call accepts only an application in the `PUBLISHED` status — for any other status the response is `404 NOT_FOUND`.
2. **Rebind servers to another key** if the application key has running servers. While they exist, deletion responds with `409 APP_HAS_ACTIVE_SERVERS` and lists the blocking servers in `details.servers`.
3. **Delete the application** — `DELETE /v1/apps/:id`, response `204 No Content`. The application authorization key stops working immediately: requests made with it return `401 KEY_INACTIVE`. Deleting the same identifier again responds with `404 APP_NOT_FOUND`.

```bash
# 1. Unpublish — placements will be unbound from the account
curl -X POST "https://vibecode.bitrix24.com/v1/apps/APP_ID/unpublish" \
  -H "X-Api-Key: YOUR_API_KEY"

# 2. Delete the application and revoke its authorization key
curl -X DELETE "https://vibecode.bitrix24.com/v1/apps/APP_ID" \
  -H "X-Api-Key: YOUR_API_KEY"
```

If the application may be needed again, stop at the first step: [publishing](/docs/apps/publish) it again with an empty body returns it to the catalog.

Manual [placement unbinding](/docs/apps/placements/unbind) serves a different purpose — removing a single interface point from an application that stays in service.

### Replace one application with another

A new application is a separate registration on the account with its own authorization key and its own placement list. Follow this order so employees never lose access:

1. [Create an application](/docs/apps/create) with the scope set you need, deploy the code, set `appUrl`.
2. Authorize the new application on the account via OAuth — without that, publishing responds with `400 NO_USER_TOKEN`.
3. [Publish](/docs/apps/publish) the new application with the same placement codes.
4. Unpublish and delete the old application following the steps above.

### Merge several applications into one

The scope set is defined when the application is created. A scope cannot be added to an existing application: [updating the application](/docs/apps/update) with an extended set responds with `403 OAUTH_SCOPE_CHANGE_REQUIRES_REISSUE`. The same call does allow narrowing the set. A merge is therefore a new application with the combined scope set, not a rework of one of the existing ones.

The order for merging three applications into one:

1. Collect the scope sets and placement codes of the three applications into a single list. The current values come from the [application list](/docs/apps/list).
2. [Create](/docs/apps/create) a new application with the combined scope set, deploy the code, set `appUrl`.
3. Authorize the new application on the account via OAuth and [publish](/docs/apps/publish) it with all placement codes at once.
4. Move the integrations to the key of the new application. The keys of the three old applications stop working the moment those applications are deleted.
5. Unpublish and delete the three old applications.

During the transition, the old and the new applications occupy slots in the same key counter. If it is exhausted, creation responds with `409 KEY_LIMIT_REACHED`. If `error.details.scope` is absent, free a user slot by deleting one of the old applications. `scope: "portal"` means the platform's per-account cap, and deletion helps only once the total number of live keys on the account drops below the limit.

---

## Placements

A placement is a point in the Bitrix24 interface where your application opens: a tab in a deal card, a left-menu item, a button on a list toolbar, a panel in a chat. Publishing binds the placements listed in the `placements` field, while separate [bind and unbind](/docs/apps/placements) calls let you manage them one at a time and change the label and the handler address without republishing.

A few conditions must be met before the first bind — each unmet condition returns its own error code.

| Condition | What happens if it is not met |
|---------|------------------------------|
| The application authorization key `vibe_app_…`. A personal key cannot bind a placement | `400 OAUTH_APP_REQUIRED` |
| The `placement` scope on the key — required for binding and unbinding, not for reading the list | `403 PLACEMENT_SCOPE_MISSING` |
| A Bitrix24 scope for the placement group: `crm` for CRM cards and lists, `im` for chats, `task` for tasks, `contact_center` for the Contact Center | `403 PLACEMENT_APP_GRANT_MISSING` — the name of the missing right arrives in `error.details.requiredScope`. An application's rights are fixed at install and cannot change afterwards, so add the right before reinstalling or calling `relink-oauth` |
| A session token alongside the key: some accounts perform the bind with the key alone, others also require the `Authorization: Bearer` header | `401 SESSION_REQUIRED` |
| A commercial Bitrix24 plan — the exact condition for the account is returned up front by the `placements.bindPrerequisite` block in the [key data](/docs/keys-auth/me). Infrastructure and key issuance additionally require a Vibe+ plan (`INT_VIBE_PLUS_REQUIRED`) | `403 INT_TARIFF_REQUIRED` |

The conditions above apply to binding and unbinding. Listing bound placements requires only the application key, and the [Available placements](/docs/apps/placements/available) reference is the exception — it is open to any valid key, and its code list is best read from a live call: it keeps growing, and there is no need to hardcode it. Details on each operation — [Placements](/docs/apps/placements).

---

## Known specifics

- **An application belongs to a single account.** The application record is created on the account whose key is passed at [creation](/docs/apps/create), and it acts on behalf of users of that same account. To make the same application work on another account, register it there with a separate [`POST /v1/apps`](/docs/apps/create) call using that account's key. A distributable application is one codebase deployed as a separate record on each account.
- **The scope set is fixed at creation.** It cannot be changed for an existing application — to work with a different scope set, recreate the application. Without `placement` in the set, publishing returns `MISSING_SCOPE`, and a request that needs a scope outside the set returns `BITRIX_ACCESS_DENIED`. More on the [Scopes](/docs/scopes) page.
- **Placements with smart processes.** Dynamic codes of the form `CRM_DYNAMIC_<entityTypeId>_DETAIL_TAB` are accepted on par with static ones — the identifier depends on the account.
- **The exact list of placement codes.** `placements` accepts only codes from a fixed set — the current list is returned by [Available placements](/docs/apps/placements/available). Codes outside the set return `400 VALIDATION_ERROR` on binding. If the code you need is not in `/available`, it is not supported.
- **`appUrl` may contain a path.** The value can be a bare subdomain or an address with a path, for example `https://app-abc12345.vibecode.bitrix24.com/hh-connector`. This is a working technique for several applications behind one Black Hole subdomain: split them by paths via a reverse proxy, and give each application its own `appUrl` with the path you need.
- **An Open Channels connector needs the `imopenlines` scope.** For an application that registers an external-messenger connector for Open Channels, add `imopenlines` to the scope set. The full list of available scopes — on the [Scopes](/docs/scopes) page.

---

## Windows / PowerShell and UTF-8

Non-ASCII characters (accented letters such as ö, ü, é, or characters from any non-Latin script) in the application `title` may turn into question marks (`?`) if the request is sent from Windows PowerShell without explicit UTF-8 serialization. This is not a display problem on the Bitrix24 account side — the bytes are lost before the HTTP request is sent, on the client side.

**Cause.** By default, PowerShell re-encodes the string from the `-Body` parameter of `Invoke-WebRequest` and `Invoke-RestMethod` into the system's legacy (non-UTF-8) code page — for example `windows-1252` on Western installs — and any character outside that page is lost before the request is assembled. The `Content-Type: charset=utf-8` header does not help here — by the time it applies, the original bytes are already lost.

**Solution.** Pass the request body as a UTF-8 byte array.

```powershell
# 1. Console output encoding — does not affect how the body is encoded
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 2. Build the JSON and convert it to a UTF-8 byte array
$body = @{
  title  = 'Sales dashboard'
  scopes = @('crm', 'user', 'placement')
} | ConvertTo-Json -Compress

$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)

# 3. Pass a byte array to -Body (not a string) and set the encoding in Content-Type
Invoke-WebRequest `
  -Uri 'https://vibecode.bitrix24.com/v1/apps' `
  -Method POST `
  -Headers @{
    'X-Api-Key'    = 'YOUR_API_KEY'
    'Content-Type' = 'application/json; charset=utf-8'
  } `
  -Body $bytes
```

**Common mistakes:**

- Saving the `.ps1` with a UTF-8 BOM — older PowerShell versions may fail to parse the script itself.
- Passing a string to `-Body` (`-Body $body`) instead of a byte array (`-Body $bytes`) — the string is re-encoded through the system code page.
- Relying only on `Content-Type: application/json; charset=utf-8` without `UTF8.GetBytes` — this header does not restore the lost bytes, it only declares the body encoding to the server.

The same serialization is needed when [updating an application](/docs/apps/update) — `title` changes via `PATCH /v1/apps/:id` with the same UTF-8 body.

**Node.js (`fetch`) and Python (`requests`)** encode the body in UTF-8 themselves. No extra steps are required. The problem is specific to PowerShell.

---

## Error codes

Each endpoint page carries its own error table. Below are the general codes returned by any endpoint in this section.

| Code | HTTP | Description |
|-----|------|----------|
| `MISSING_API_KEY` | 401 | The `X-Api-Key` header is missing |
| `INVALID_API_KEY` | 401 | The key is not recognized — no such key exists on the platform |
| `RATE_LIMITED` | 429 | Request limit exceeded |
| `BITRIX_UNAVAILABLE` | 502 | The Bitrix24 portal is unavailable |
| `INTERNAL_ERROR` | 500 | Internal server error |

The full list of general API errors — [Errors](/docs/errors).

---

## Endpoint reference

| Method | Path | Description |
|-------|------|----------|
| GET | [/v1/apps](/docs/apps/list) | List of applications on the account |
| POST | [/v1/apps](/docs/apps/create) | Create an application |
| GET | [/v1/apps/:id](/docs/apps/get) | Application data |
| PATCH | [/v1/apps/:id](/docs/apps/update) | Update an application |
| DELETE | [/v1/apps/:id](/docs/apps/delete) | Delete an application |
| POST | [/v1/apps/:id/publish](/docs/apps/publish) | Publish to the catalog |
| POST | [/v1/apps/:id/unpublish](/docs/apps/unpublish) | Unpublish |
| POST | [/v1/apps/:id/relink-oauth](/docs/apps/relink-oauth) | Relink the application to the account's OAuth application |
| GET | [/v1/placements](/docs/apps/placements/list) | Placements bound by the application |
| GET | [/v1/placements/available](/docs/apps/placements/available) | Available placements reference |
| POST | [/v1/placements/bind](/docs/apps/placements/bind) | Bind a placement |
| POST | [/v1/placements/unbind](/docs/apps/placements/unbind) | Unbind a placement |

An interactive method switcher with examples and response fields — [Endpoints](/docs/apps/endpoints).

## See also

- [Placements](/docs/apps/placements)
- [App management](/docs/apps/endpoints)
- [Keys and authorization](/docs/keys-auth)
- [Scopes](/docs/scopes)
- [Deploy API](/docs/infra/deploy)
- [Source code storage](/docs/source-storage)
- [Bitrix24 event subscriptions](/docs/infra/event-subscriptions)
