
## Publish the app to the Bitrix24 catalog

`POST /v1/infra/servers/:id/b24-catalog/publish`

Creates the app card in the Vibecode apps catalog on the Bitrix24 account. Before this call the card appeared only after a successful [deployment](/docs/infra/deploy/deploy), so an app brought up any other way was missing from the catalog, and granting access to employees did not surface it.

Editing the [access policy](/docs/infra/access/access-policy) or the access list does not create a card — it updates an existing one. Publishing stays a separate action: the card appears in the catalog for the owner and for the employees who already have access.

This is not REST app publishing and not an interface placement — those are `POST /v1/apps/:id/publish` and [placements](/docs/apps/placements).

## Parameters

| Parameter | In | Type | Req. | Default | Description |
|-----------|----|------|:----:|---------|-------------|
| `id` | path | string (UUID) | yes | — | ID of a server that has no catalog card yet |

The request body may be empty or `{}`. Unknown fields are ignored.

## Examples

### curl — personal key

```bash
curl -X POST -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" -d '{}' \
  https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/b24-catalog/publish
```

### curl — OAuth application

```bash
curl -X POST -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" -d '{}' \
  https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/b24-catalog/publish
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/b24-catalog/publish`,
  {
    method: 'POST',
    headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: '{}',
  }
)
const body = await res.json()
if (!body.success) {
  // CATALOG_ALREADY_PUBLISHED means the card is already there — do not repeat
  console.error(body.error.code, body.error.message)
  throw new Error(body.error.code)
}
// The card appears in the catalog after the next synchronization cycle
console.log('Publication queued')
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/b24-catalog/publish`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: '{}',
  }
)
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` when the publication is accepted |
| `data.queued` | boolean | `true` — the publication is queued. The card is created by background synchronization, not by this call |
| `data.op` | string | The queued operation. For this endpoint it is always `ADD` |

The publication state is read through [`GET /v1/infra/servers/:id`](./get.md) in the `b24CatalogSync` block:

| Field | Type | Description |
|-------|------|-------------|
| `b24CatalogSync.itemId` | number \| null | ID of the card in the Bitrix24 catalog. `null` — there is no card. This is the "published" signal, the other fields describe the process |
| `b24CatalogSync.status` | string | `IDLE` — queued or nothing to synchronize, `PENDING` — being processed, `SYNCED` — the card is synchronized, `FAILED` — synchronization did not succeed, `ORPHANED` — the card was removed on the Bitrix24 side |
| `b24CatalogSync.pendingOp` | string \| null | The queued operation: `ADD`, `UPDATE`, `DELETE`. `null` — the queue is empty |
| `b24CatalogSync.attempts` | number | Number of synchronization attempts for the current operation |
| `b24CatalogSync.eligible` | boolean | `false` when a card is impossible: the server has no subdomain, no app has been deployed to it yet, it is an agent runtime or a galaxy host, it was switched to `OPEN` mode, it has no managing key or account, or catalog synchronization is switched off on the platform side. With `false` the publish call answers `400 CATALOG_NOT_ELIGIBLE` |

A value of `eligible: true` means "a card is possible", not "publishing will succeed": for an app whose card already exists the call answers `409 CATALOG_ALREADY_PUBLISHED`.

## Response example

```json
{
  "success": true,
  "data": {
    "queued": true,
    "op": "ADD"
  }
}
```

## Error response example

409 — the catalog card already exists:

```json
{
  "success": false,
  "error": {
    "code": "CATALOG_ALREADY_PUBLISHED",
    "message": "This server already has a Bitrix24 catalog card."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|------|-------------|
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The API key is wrong or expired |
| 400 | `CATALOG_NOT_ELIGIBLE` | A card is impossible for this server: no subdomain, no app deployed yet, an agent runtime or a galaxy host, the server is in `OPEN` mode, it has no managing key or account, or catalog synchronization is switched off on the platform side |
| 400 | `CATALOG_ORPHANED` | The card was removed on the Bitrix24 side. Restoring it is available in the Vibecode dashboard, not on this endpoint |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | The call was made with a Cowork/Code key — such a key works with data only and cannot perform write operations. To issue a key that can, see [Project key for deploy](/docs/cowork/deploy-key) |
| 409 | `CATALOG_ALREADY_PUBLISHED` | The card already exists. There is no need to repeat the call |
| 409 | `CATALOG_DELETE_PENDING` | A catalog delete is already queued — publishing is unavailable in this state |
| 404 | `NOT_FOUND` | The server is deleted or managed by another API key |
| 429 | `RATE_LIMITED` | Request limit exceeded. The response carries a `Retry-After` header with the recommended pause |

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

## Known specifics

- **The call queues a task instead of creating the card.** The card is created by background synchronization, so `itemId` in [`GET /v1/infra/servers/:id`](./get.md) is not filled at response time. Track readiness by polling: `b24CatalogSync.itemId` stops being `null` and `status` becomes `SYNCED`.
- **Repeating the call while the publication is queued is safe.** The answer is `200` again and no second card is created.
- **A managing key is required.** Publishing runs on behalf of the owner of the server's managing key, so a server that lost its key answers `400 CATALOG_NOT_ELIGIBLE`. The key is restored in your Vibecode account.
- **Granting access does not create the card.** The order is: publish first, then the [access policy](/docs/infra/access/access-policy) and the access list. The reverse order also works — after publishing, the card picks up the access already granted on the first synchronization.
- **Deploy first, publish second.** The subdomain is minted when the server is created, and an app only appears on it after a [deploy](/docs/infra/deploy/deploy), so a server with no deploy on record returns `eligible: false` and the call answers `400 CATALOG_NOT_ELIGIBLE`. Otherwise the catalog card would lead to an address where nothing answers, and the only way to remove it would be to delete the server. The platform also holds no deploy record when the app arrived not through `deploy` but via a file upload, a command or SSH — such a server gets `400 CATALOG_NOT_ELIGIBLE` too, even though the app is running on it. The only way around is a real deploy, and by default (`cleanDeploy`) it wipes the app directory, so save whatever is already there first.
- **Catalog synchronization can be switched off on the platform side.** In that state `eligible` comes back `false` and the publish call answers `400 CATALOG_NOT_ELIGIBLE`: nothing would process the queue, so the platform refuses up front instead of leaving the card "being added" indefinitely.

## See also

- [Get a server](./get.md)
- [Access policy](/docs/infra/access/access-policy)
- [Access list](/docs/infra/access/access-list)
- [Deploy an app](/docs/infra/deploy/deploy)
