# Application catalog

The section gives you the basis for a "my applications" screen inside your own product: one request returns both the person's own applications and other people's applications that this person can access. It has two operations: a list and a card for a single application.

There is no other way to assemble that list: [`GET /v1/infra/servers`](/docs/infra/servers/list) is scoped to the calling key, so an application created in the Vibecode dashboard is not visible there. Every row also carries the state you would otherwise have to fetch separately: the server, the saved source versions, whether an operation is in flight, and a ready-made address for opening the application. The section is read-only — creating, editing and deleting applications are not part of it.

**Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** `X-Api-Key` | **Key:** an API key or an authorization key

[What the selection contains](#what-the-selection-contains) | [What a card carries](#what-a-card-carries) | [Identifiers](#identifiers-what-matches-what) | [Ordering and walking pages](#ordering-and-walking-pages) | [Rate limiting](#rate-limiting) | [Quick start](#quick-start) | [Full example](#full-example) | [Endpoint reference](#endpoint-reference) | [Error codes](#error-codes)

## What the selection contains

The selection is scoped to the **key owner**, not to the key itself. That is the main difference from [Servers](/docs/infra/servers): there the output is limited to the servers of the current key, so an application created in the Vibecode dashboard is not visible there — its server is bound to another key of the same person. Here such an application is present, and issuing a new key does not reset the list.

The section works with a key bound to a Bitrix24 account: either an API key `vibe_api_…` or an authorization key `vibe_app_…` will do. This section needs no session token. Under an authorization key the selection belongs to the key owner — whoever issued the key — and not to the employee on whose behalf the application acts. The section requires no dedicated scope. A key that is not bound to a Bitrix24 account never reaches this section — the key check refuses it with `401 NO_PORTAL`; a management key gets `403 MANAGEMENT_KEY_NO_ENTITY_ACCESS`. The full breakdown is in "Error codes" below.

What exactly comes back is set by the `scope` parameter:

| `scope` | What it covers |
|---------|----------------|
| `feed` | The combined list: your own applications and all other people's applications you can access. The default value |
| `mine` | Your own applications only |
| `shared` | Other people's applications that you can access. Besides the ones shared with you personally, this also covers applications without a server that are open to the whole Bitrix24 account |

Deleted applications are never returned in any `scope`.

On `feed` the selection is capped from above, because the order for it is computed by the platform rather than by the database. So `total` there is the size of the selection, not the number of applications on the Bitrix24 account, and the list envelope says so through `truncated`. On `mine` and `shared` there is no ceiling.

The viewer's relation to each row arrives in the `viewerState` field: `owner` — the application is yours, `shared` — access was granted to you personally or through the server, `usable` — the application has no server and is open to the whole Bitrix24 account, `requestable` — no access. When an application has a server, its access is decided by the server policy, so an application open to everyone arrives as `shared` rather than `usable`. The section returns no separate "may I manage this" flag — it only reads. `viewerState` itself is what distinguishes your own applications from other people's.

## What a card carries

A list row and a single application card have the same shape, so a client does not need to know about two different objects. Beyond the name, description and icon, that shape carries the opening address and three blocks of state.

**`openUrl` and `openTarget`** — one answer to "where do I open this application". There is a server with an address — `openUrl` returns that address and `openTarget` is `app`. In every other case both fields arrive `null`, and an empty slot here is the normal answer rather than a failure.

Both fields are also `null` for an application registered at one of the Bitrix24 account's [placements](/docs/apps/placements), that is, one that opens inside the Bitrix24 interface: the platform does not yet know the address such an application is opened at, and it will not invent one in place of an honest `null`. Substituting `server.url` there yourself is not an option either — the [Application card](./applications/get.md) explains why.

**`isEmbedded`** is the flag that tells those two cases apart. An empty open pair arrives both for an embedded application and for one that has not been published yet, and a person must be shown different things: "opens inside Bitrix24" versus "nowhere to open it yet". Do NOT infer embedding from the presence of a server: an embedded application with no server of its own is an ordinary state — it was embedded before the code was ever deployed — and the flag does not depend on the server. The field is disclosed to everyone who can see the card, not only to the owner.

**`server`** — a summary of the application server: id, state, address, resource type and the `reachable` flag. `null` when there is no server or it was deleted. The `reachable` field does not answer the same question as `status`: `status` says the container is up, `reachable` says it also answers over the network. They diverge in practice, not in theory — a container can be up while the tunnel to it never came up at all, and by `status` alone the application looks fine.

The flag **already implies** `status === "RUNNING"`: it never arrives true for any other state, so there is nothing to conjoin it with on the client.

⚠️ **For an application on a shared galaxy host (`server.kind: "GALAXY_APP"`) the second half of the flag is taken from the HOST, not from the container itself.** The connectivity there is held by the host; the container has none of its own by design. So `true` means three things at once: the container runs, the host runs, and the host's tunnel is up. Consequence worth knowing: a just-created container that has not reached `RUNNING` yet (it only does so after its first source upload) arrives with `reachable: false` even on a perfectly healthy host. That is "the container is not up yet", not "the host is unreachable", and this flag alone does not tell the two apart — read `status` for that.

**`iconUrl`** — a host-less relative path, e.g. `/api/app-icons/7f3…?v=1755500000000`, or `null` when the card holds no icon pointer. The serve endpoint is anonymous: the link needs no key, carries no signature and never expires, so it is enough to join the path with your API base URL and drop it straight into `<img src>` — no need to proxy it through a server of your own. A 256×256 PNG is returned with `Cache-Control: public, max-age=300, must-revalidate` and an ETag, so card caching is not broken: after five minutes the client gets a cheap `304`. The placeholder for `null` is the client's to draw — the platform does not invent an icon.

**`sources`** — a summary of the saved source versions. The `latestVersionId` field arrives in the `v<N>` shape — exactly the shape [`GET /v1/infra/servers/:id/sources/:versionId/download`](/docs/source-storage) accepts. It is not the record's primary key: put it into the download address and you get the latest version, with no separate call for the version list.

**`activeOperation`** — the operation in flight on the server: kind, step and start time. And here is the caveat that saves hours of debugging: **`null` means "there is no operation with a stored record", not "nothing is happening to this application"**. The field is reliable for deployment, repair, a server plan change and moving a container between galaxies — the platform does not log anything else, so no other action appears in this field. The third state is `status: "unknown"`: the operation did start, but its outcome is unknown, so waiting for it to finish is pointless.

Both of those last blocks are filled in only for whoever manages the application. A viewer the application was merely shared with gets an empty `sources` (`hasVersions: false`, both fields `null`) and `activeOperation: null`. The response shape does not change, so an empty summary on its own does not mean "there are no versions". It has three causes, and from the outside they look identical: the application has no server, there really are no versions, or the data is not disclosed to you because the application is someone else's.

## Identifiers: what matches what

The section returns two identifiers, and both lead into other API sections — just not where the field name suggests.

**`server.id` is the same identifier the [Servers](/docs/infra/servers) paths take.** Put it straight into `GET /v1/infra/servers/:id` and into the source-download path. One caveat: the format matches, the access right does not — that endpoint is scoped to the calling key's own servers, so a server this section showed you (it selects by key OWNER, not by key) may answer `404` there.

**The application `id` and the `id` from [`GET /v1/apps`](/docs/apps) are DIFFERENT values of different entities.** Here it is an application card; there it is the registration of an OAuth application on the Bitrix24 account, and even their formats differ. A bridge between them exists inside the platform, but this section deliberately does not expose it. Practical consequence: putting an `id` from this section into `/v1/apps/...` paths yields a `404`, not somebody else's data.

⚠️ **Two names for one meaning.** The application name arrives here as `name`, while in `GET /v1/apps` the registration heading is called `title`. Those are two independent fields of two entities: they are not synchronized and may diverge, so do not carry one over into the other.

⚠️ **Sources: two paths, and `:appId` in the first is not what it looks like.** A version is written with `POST /v1/apps/:appId/sources`, where `:appId` is the OAuth application's identifier — NOT the application from this section and not the server. Reading and downloading a version go by server: `GET /v1/infra/servers/:id/sources/:versionId/download`, and that is where `server.id` from here goes, together with `sources.latestVersionId`. Both paths are live; neither replaces the other.

## Ordering and walking pages

The order is stated here in full, because whether you can walk the list page by page and get every application exactly once depends on it.

| `scope` | Order |
|---------|-------|
| `feed` | Pinned by you → your own → other people's. Within a group, by `updatedAt` newest first, ties broken by `id` |
| `mine`, `shared` | By `createdAt` newest first, ties broken by `id` |

⚠️ **`updatedAt` only moves when the card itself is edited — a deployment does not touch it.** So "freshness" in the feed means "when the card was last changed", not "when the application was last deployed": an application deployed a minute ago stays exactly where it was. When you need the freshness of the code, read `sources.latestSavedAt`.

The trailing `id` key is not a formality. Applications created in a batch carry identical timestamps, and without a secondary key two pages of one walk could overlap or skip an application: the database promises nothing about the order within a group of equal timestamps. With it, the walk is reproducible.

## Rate limiting

| Operation | Fleet-wide limit | One replica's share today |
|-----------|------------------|---------------------------|
| [`GET /v1/applications`](/docs/applications/list) | 60 requests per minute | 20 |
| [`GET /v1/applications/:id`](/docs/applications/get) | 120 requests per minute | 40 |

⚠️ **The left-hand numbers are the platform-wide limit, not the one your client will see.** Requests are served by several backend replicas; each holds its own share of the limit and its own counter, independent of its neighbours: there are three replicas today, so a single replica passes 20 list requests per minute and 40 card requests. The full 60 and 120 are reached only when requests spread evenly across replicas; a client whose connection sticks to one replica will hit that replica's share sooner.

So **plan from the header, not from the table**: `X-RateLimit-Limit` carries the actual limit of the replica that served you, `X-RateLimit-Reset` the number of seconds until the window closes. The replica count may change without an entry in the changelog; the header never lies.

**The counter is per key OWNER within a portal — not per IP address and not per individual key.** That matters in both directions: several keys belonging to one person share a single counter (issuing a new key does not reset the limit), and employees of one portal never get in each other's way — even when all their requests leave for the internet through a single address. The list and the card keep separate counters.

Exceeding the rate arrives as `429` with the code `RATE_LIMITED` and a `Retry-After` header (whole seconds until the window closes).

⚠️ **`429` is not always about rate, so branch on `error.code` rather than on the status.** The key's daily free-call quota answers with the same status but the code `QUOTA_EXCEEDED`, and waiting does not cure it — a paid plan does. A client that shows every `429` as "too fast, please wait" will wait forever on an exhausted quota; a client that shows every `429` as "quota exhausted" will frighten a person during an ordinary rate spike.

## Quick start

Your own applications in a single request:

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/applications?scope=mine&limit=20"
```

```json
{
  "success": true,
  "data": [
    {
      "id": "cmsw806qi0000tdskiw2aegii",
      "name": "Deal report",
      "type": "PERSONAL",
      "viewerState": "owner",
      "pinned": false,
      "author": { "name": "Application author" },
      "isEmbedded": false,
      "openUrl": "https://app-05b67cf7.vibecode.bitrix24.com",
      "openTarget": "app",
      "server": {
        "id": "5cbb50f9-f95f-4ddf-ba3b-b771209cb6fb",
        "status": "RUNNING",
        "url": "https://app-05b67cf7.vibecode.bitrix24.com",
        "kind": "STANDALONE",
        "reachable": true,
        "lastDeployedAt": "2026-08-16T19:50:41.302Z"
      },
      "sources": { "hasVersions": true, "latestVersionId": "v2", "latestSavedAt": "2026-08-16T19:41:18.818Z" },
      "activeOperation": { "kind": "deploy", "status": "running", "step": "build", "startedAt": "2026-08-16T19:50:33.822Z" }
    }
  ],
  "total": 2,
  "page": 1,
  "limit": 20,
  "truncated": false
}
```

The example shows one application and its main fields. The full breakdown — [Application card](./applications/get.md).

## Full example

The script collects all your own applications page by page, shows the ones with an operation in flight, and takes the address of the latest source version from one of them.

```javascript
const BASE = 'https://vibecode.bitrix24.com/v1'
const API_KEY = process.env.VIBE_API_KEY

async function api(path) {
  const res = await fetch(`${BASE}${path}`, { headers: { 'X-Api-Key': API_KEY } })
  const body = await res.json()
  if (!res.ok) throw new Error(`${path}: ${body.error.code} — ${body.error.message}`)
  return body
}

// 1. Your own catalog: walk the pages until every application is collected.
const applications = []
let page = 1
let total = 0
do {
  const chunk = await api(`/applications?scope=mine&page=${page}&limit=100`)
  applications.push(...chunk.data)
  total = chunk.total
  page += 1
} while (applications.length < total && page <= 100)

console.log(`Applications: ${applications.length} of ${total}`)

// 2. Applications with an operation in flight right now.
for (const app of applications.filter(a => a.activeOperation !== null)) {
  const { kind, status, step, startedAt } = app.activeOperation
  console.log(`${app.name}: ${kind} — ${status}, step ${step ?? 'not reported'}, started ${startedAt}`)
}

// 3. One application card: where to open it and the address of its latest source version.
const target = applications.find(a => a.server !== null && a.sources.hasVersions)
if (target) {
  const { data: card } = await api(`/applications/${target.id}`)
  console.log(`${card.name}: open at ${card.openUrl ?? 'nowhere'} (${card.openTarget ?? 'no address'})`)
  console.log(`Server ${card.server.status}, answers over the network: ${card.server.reachable}`)
  console.log(`Version ${card.sources.latestVersionId} saved ${card.sources.latestSavedAt}`)
  console.log(`Download: ${BASE}/infra/servers/${card.server.id}/sources/${card.sources.latestVersionId}/download`)
}
```

Output for the Bitrix24 account used in the example:

```
Applications: 2 of 2
Deal report: deploy — unknown, step build, started 2026-08-16T19:50:33.822Z
Deal report: open at https://app-05b67cf7.vibecode.bitrix24.com (app)
Server RUNNING, answers over the network: true
Version v2 saved 2026-08-16T19:41:18.818Z
Download: https://vibecode.bitrix24.com/v1/infra/servers/5cbb50f9-f95f-4ddf-ba3b-b771209cb6fb/sources/v2/download
```

## Operations

- [List applications](./applications/list.md) — `GET /v1/applications`
- [Application card](./applications/get.md) — `GET /v1/applications/:id`

## Endpoint reference

| Method | Path | Description |
|--------|------|-------------|
| GET | [/v1/applications](/docs/applications/list) | Applications of the key owner: their own, shared by colleagues, or everything together |
| GET | [/v1/applications/:id](/docs/applications/get) | One application: server, saved sources, operation in flight |

## Error codes

| HTTP | Code | Description |
|------|------|-------------|
| 400 | `INVALID_SCOPE` | The `scope` value is not one of `mine`, `shared`, `feed` |
| 401 | `NO_PORTAL` | The key is not bound to an account. This is the actual answer for "a key with no account" — such a request never reaches the handler |
| 403 | `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | A management key: entity sections are out of its reach, use a personal key or an authorization key |
| 403 | `PORTAL_KEY_REQUIRED` | The section's own safety net. It fires only on a damaged key row (no owner); the ordinary "key with no account" arrives as `401 NO_PORTAL` above |
| 403 | `FORBIDDEN` | The application exists, but the viewer has no access to it |
| 404 | `APPLICATION_NOT_FOUND` | No application with that `id` on the key's portal |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not sent |
| 401 | `INVALID_API_KEY` | Invalid or expired API key |
| 429 | `RATE_LIMITED` | Rate exceeded. A `Retry-After` header is present — retrying after the stated pause is normal |
| 429 | `QUOTA_EXCEEDED` | The key's daily free-call quota is exhausted. Same status, but waiting will not help — this is not about rate |

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

## See also

- [Servers](/docs/infra/servers)
- [Source storage](/docs/source-storage)
- [Keys and authorization](/docs/keys-auth)
- [Limits and optimization](/docs/optimization)
- [Errors](/docs/errors)
