
## Publish app

`POST /v1/apps/:id/publish`

Moves the app to `PUBLISHED`, binds placements to the Bitrix24 account, and makes the app visible to all employees.

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|:-----:|---------|
| `id` (path) | string | yes | App identifier. List: `GET /v1/apps` |

## Request fields (body)

The body is optional. With an empty body, values are taken from the app record — this is the primary scenario for re-publishing without changes. If you send a body, add the `Content-Type: application/json` header — without it, a request with a body returns `413 FST_ERR_CTP_BODY_TOO_LARGE`.

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `catalogTitle` | string | no | Title in the Bitrix24 account catalog. If not sent — the app's current value |
| `catalogDescription` | string | no | Catalog description |
| `catalogIcon` | string | no | Catalog icon code |
| `appUrl` | string | no | App address on Black Hole. If not sent — the app's current `appUrl` |
| `placements` | string[] | no | Placement codes. List of allowed codes — [Available placements](/docs/apps/placements/available). If not sent — the app's current set |
| `sourceVersionId` | string | no | Specific source snapshot to publish, format `v<number>`. Applies when [source storage](/docs/source-storage) is enabled |

## Examples

### curl — personal key

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

### curl — OAuth app

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

### JavaScript — personal key

```javascript
const res = await fetch(
  'https://vibecode.bitrix24.com/v1/apps/YOUR_APP_ID/publish',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ placements: ['CRM_DEAL_DETAIL_TAB'] }),
  }
)
const body = await res.json()
if (!body.success) throw new Error(body.error.code)
// body.warnings — array, present if some placements failed to bind
console.log(body.data.placements)
```

### JavaScript — OAuth app

```javascript
const res = await fetch(
  'https://vibecode.bitrix24.com/v1/apps/YOUR_APP_ID/publish',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ placements: ['CRM_DEAL_DETAIL_TAB'] }),
  }
)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | `true` on successful publication |
| `data` | object | The app after publication — the same 17 fields returned by [app data](/docs/apps/get) |
| `data.placements` | string[] | Bound placements |
| `data.catalogStatus` | string | After publication — `PUBLISHED` |
| `data.publishedAt` | string \| null | Publication date, ISO 8601 |
| `warnings` | string[] | Present if some placements could not be bound. Lists the codes that did not bind |

## Response example

The `data` field mirrors the app object with a filled `placements` array:

```json
{
  "success": true,
  "data": {
    "id": "33c4d5e6-f7a8-49b0-1234-5c6d7e8f9012",
    "title": "Sales dashboard",
    "description": null,
    "scopes": ["crm", "user", "placement"],
    "handlerUrl": "https://vibecode.bitrix24.com/v1/bitrix-handler",
    "appUrl": "https://app-abc12345.vibecode.bitrix24.com",
    "redirectUris": [
      "https://vibecode.bitrix24.com/oauth/complete",
      "http://localhost"
    ],
    "bitrixClientId": "local.7c3d4e5f6a7b80.55556666",
    "prefix": "vibe_app_local_7c3",
    "suffix": "6666",
    "authorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "portalId": "8b1f0e2a-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
    "createdAt": "2026-06-24T09:12:45.781Z",
    "updatedAt": "2026-06-24T10:03:18.204Z",
    "placements": ["CRM_DEAL_DETAIL_TAB"],
    "catalogStatus": "PUBLISHED",
    "publishedAt": "2026-06-24T10:03:18.204Z"
  }
}
```

## Error response example

400 — the OAuth key lacks the `placement` scope:

```json
{
  "success": false,
  "error": {
    "code": "MISSING_SCOPE",
    "message": "OAuth API key must have the placement scope to publish (catalog publishing binds placements on Bitrix24)."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 404 | `NOT_FOUND` | App not found or belongs to another portal |
| 403 | `FORBIDDEN` | Request is not from the app author and not from a Bitrix24 account administrator |
| 409 | `ALREADY_PUBLISHED` | App is already in `PUBLISHED` status |
| 400 | `NO_OAUTH_KEY` | The app has no associated OAuth key |
| 400 | `MISSING_SCOPE` | The OAuth key lacks the `placement` scope |
| 409 | `SNAPSHOT_REQUIRED` | Source storage is enabled, but there is no fresh snapshot. The body carries a `hint` with steps for `POST /v1/apps/:id/sources` |
| 400 | `NO_USER_TOKEN` | The app is not authorized on the portal via OAuth. Complete authorization and retry publication — [Application user authorization](/docs/keys-auth/oauth) |
| 400 | `TITLE_TOO_LONG_FOR_CATALOG` | The resolved title is longer than 100 characters. This happens when `catalogTitle` is not sent and the app's `title` exceeds the catalog limit |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **Publication requires the app to be authorized on the Bitrix24 account.** Placement binding is performed by the app's OAuth token, so before the first publication the app is authorized on the Bitrix24 account at least once. Without it, publication returns `NO_USER_TOKEN`. The authorization sequence — [Application user authorization](/docs/keys-auth/oauth). Consent on the Bitrix24 account is enough to publish. You do not need to fetch the session token — the platform stores the app token on return.
- **The placement title is the app name.** The left-menu item and the CRM tabs are labelled with the resolved `catalogTitle`; there is no separate name for the menu. Renaming the app through [update](/docs/apps/update) also changes the placement labels.
- **Unpublished apps are published again.** Publication is accepted in `UNPUBLISHED` status too — the app returns to `PUBLISHED` with the same or new placements. Catalog metadata is preserved on unpublish, so with an empty body the catalog is restored without refilling it.
- **Some placements may not bind.** Each code is bound separately. Codes that could not be bound do not interrupt publication — the app becomes `PUBLISHED`, and the codes that did not bind are listed in `warnings`.

## See also

- [Apps](/docs/apps)
- [Application user authorization](/docs/keys-auth/oauth)
- [Unpublish](/docs/apps/unpublish)
- [Update app](/docs/apps/update)
- [App data](/docs/apps/get)
- [Source storage](/docs/source-storage)
- [Event subscriptions](/docs/infra/event-subscriptions)
