
# Partner Connect

Connecting external services to Bitrix24: your application asks the user for permission to access their Bitrix24 account and receives a Vibecode API key for subsequent calls. The flow is built on the Authorization Code scheme — the same one used by OAuth providers like Google or GitHub.

**Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** the partner's `client_id` + `client_secret` | **Key scopes:** requested on the consent page

## Contents

- [Connection mode](#connection-mode)
- [Company connection polling](#company-connection-polling)
- [When to use](#when-to-use)
- [How it works](#how-it-works)
- [Endpoint reference](#endpoint-reference)
- [Full scenario](#full-scenario) — an Express handler end to end
- [Connecting AI from an application inside Bitrix24](#connecting-ai-from-an-application-inside-bitrix24) — a button and consent in a separate window
- [`GET /v1/connect/authorize`](#get-v1-connect-authorize) — flow start, redirect to consent
- [`POST /v1/connect/token`](#post-v1-connect-token) — exchanging the code for an API key
- [Public client: the PKCE flow](#public-client-the-pkce-flow) — an application with no server
- [Signing in from a device without a browser](#signing-in-from-a-device-without-a-browser) — TV, set-top box, terminal
- [Using the API key](#using-the-api-key)
- [Available scopes](#available-scopes)
- [Key lifetime and revocation](#key-lifetime-and-revocation)
- [Security](#security)
- [Partner registration](#partner-registration)
- [Client types](#client-types) — public or confidential, with examples
- [See also](#see-also)

## Connection mode

New clients use `PERSONAL` mode: consent and the key belong to the user. `COMPANY` mode prepares a shared company connection to a Bitrix24 account; only a platform administrator can set this mode. It requires a `CONFIDENTIAL` client, a client secret and PKCE `S256`. Device sign-in and `COMPANY` are mutually exclusive: enabling one while the other is on is rejected. A mode change is possible until the client issues its first key; attempts without issuance do not prevent switching. The first issuance fixes the mode permanently, even after key revocation; a mode change returns HTTP 409 `CONNECT_COMPANY_MODE_LOCKED`. Register a new client for Company mode. Unfinished attempts are cancelled when an allowed mode change succeeds. An existing company connection prevents switching back to PERSONAL.

The company key is also available on self-hosted Bitrix24 portals verified by the platform.

When new issuance for `COMPANY` (`CREATE` and `RECONNECT`) is unavailable, the following refusals apply. After validating the redirect URI, `GET /v1/connect/authorize` redirects to the registered `redirect_uri` with `error=access_denied`, the original `state` and `error_description=CONNECT_COMPANY_NOT_READY` or `CONNECT_COMPANY_DISABLED`, depending on the refusal. No authorization code is created. `POST /v1/connect/token` with `grant_type=authorization_code` and valid credentials for this client returns HTTP 403 with the refusal code in `code` and `error_description`. Rotation of an existing secret has separate conditions described below.

Starting or exchanging a device code for `COMPANY` returns HTTP 400, `error=unauthorized_client` and `code=CONNECT_COMPANY_DEVICE_FLOW_FORBIDDEN`. Consent from a support session acting as another user is rejected with `CONNECT_COMPANY_IMPERSONATION_FORBIDDEN`, including on an account that permits support access. Retrying or selecting another account cannot remove these restrictions. The issuance scenarios below apply to `PERSONAL`.

A `COMPANY` credential requires explicit `vibe:storage` consent to access storage; without it, storage returns HTTP 403 `CONNECT_COMPANY_STORAGE_NOT_READY`. Storage availability is reported by `capabilities.storage` independently of administrator proof. Creating derived tokens or keys and managing external model keys are forbidden: HTTP 403 `CONNECT_COMPANY_DERIVED_TOKEN_FORBIDDEN`. Unknown routes are rejected with HTTP 403 `CONNECT_COMPANY_ROUTE_FORBIDDEN`. Selecting another Bitrix24 account cannot remove these restrictions. Issuance availability depends on the readiness of the selected connection.

A Bitrix24 company connection can be ready without the `crm` scope. At least one approved Bitrix24 scope, such as `user`, is sufficient when all administrator and connection checks pass. `bitrix_ready` does not grant access to every API route. Route availability and permission for each upstream method are checked separately.

Before each Bitrix24 call, the service checks the method against the current consent. The `task` and `tasks` scopes are equivalent. The reduced `user_basic` and `user_brief` scopes permit `user.get`; methods requiring the full `user` scope still require it. Unknown method prefixes and missing method consent return `403 SCOPE_MISSING` before any Bitrix24 request.

Company credentials can access Bitrix24 entity operations, including `/v1/users`, `/v1/departments` and `/v1/tasks`, with approved scopes. Supported entity operations include reading, creating, updating, deleting, searching, fields, aggregation, imports and batches. This does not add operations that the entity does not support. Available operations carry `x-company-access: true` in OpenAPI. OAuth-only entities and separate custom routes remain closed with `403 CONNECT_COMPANY_ROUTE_FORBIDDEN`.

The `include` parameter and custom fields are available within the current consent; data from different company connections stays separate. A missing scope returns `403 SCOPE_MISSING`. To add scopes, use `RECONNECT` with updated consent. Writes follow the normal partner-key checks without an additional READONLY restriction. A batch containing an unapproved method is rejected in full before commands are sent. Windowed search is closed for company credentials: use `autoWindow: false` for ordinary search. AI and storage availability are reported separately.

## Company connection polling

`GET /v1/connect/company/status` lets the Product server inspect its own connection using an already issued `COMPANY` credential. Send it only in the `X-Api-Key` header. A client secret, cookie or Bearer credential cannot authorize this method. Keep the connection credential on your server, out of browsers and logs. The credential determines the account and Product pair. The method accepts no query parameters, including identifiers of another pair.

### Request

```bash
curl --max-time 20 "$VIBE_URL/v1/connect/company/status" \
  -H "X-Api-Key: $VIBE_COMPANY_SECRET"
```

```javascript
const response = await fetch(`${process.env.VIBE_URL}/v1/connect/company/status`, {
  headers: { 'X-Api-Key': process.env.VIBE_COMPANY_SECRET },
  cache: 'no-store',
  signal: AbortSignal.timeout(20000),
})
const result = await response.json()
if (!response.ok) throw new Error(result.error.code)
console.log(result.data.status, result.data.revision, result.data.action)
```

Responses and errors include `Cache-Control: no-store`. Example response for a connection waiting for an administrator:

```json
{
  "success": true,
  "data": {
    "connection_id": "connection-id",
    "connection_mode": "COMPANY",
    "portal": { "domain": "example.bitrix24.com", "name": "Example" },
    "user": null,
    "status": "NEEDS_ADMIN",
    "generation": 0,
    "revision": 1,
    "credential_version": 1,
    "bitrix_ready": false,
    "reason_code": "CONNECTION_ADMIN_REQUIRED",
    "webhook_cleanup": "none",
    "capabilities": { "bitrix": false, "ai": false, "storage": false },
    "poll_after_seconds": 30,
    "action": "AUTHORIZE_ADMIN"
  }
}
```

### Response fields

| Field | Type | Meaning |
|-------|------|---------|
| `success` | boolean | `true` for successful polling |
| `data` | object | Diagnostics for your own connection |
| `data.connection_id` | string | Connection identifier returned by this method |
| `data.connection_mode` | string | Always `COMPANY` |
| `data.portal` | object | Connected Bitrix24 account: only `domain` and `name` |
| `data.portal.domain` | string | Connected Bitrix24 account domain |
| `data.portal.name` | string | Connected Bitrix24 account name, or `Unknown` when absent |
| `data.user` | object \| null | Current consent holder: only `name` and `email`, or `null` |
| `data.user.name` | string | Holder's name in their platform account |
| `data.user.email` | string | Holder's email in their platform account, or an empty string |
| `data.status` | string | `ACTIVE`, `NEEDS_ADMIN`, `SUSPENDED`, `DISCONNECTED`, `DELETION_PENDING` |
| `data.generation` | integer | Connection consent generation |
| `data.revision` | integer | Stored connection state revision |
| `data.credential_version` | integer | Current connection credential version |
| `data.bitrix_ready` | boolean | Current Bitrix24 availability |
| `data.reason_code` | string \| null | Reason Bitrix24 access is closed, or `null` when available |
| `data.webhook_cleanup` | string | `none`: no unfinished attempts, `pending`: cleanup or creation is unfinished, including when a previous webhook cannot be deleted, `attention_required`: webhook creation outcome is unknown and needs review on the platform side |
| `data.capabilities.bitrix` | boolean | Current Bitrix24 availability |
| `data.capabilities.ai` | boolean | AI availability based on scope, permission and connection state |
| `data.capabilities.storage` | boolean | Storage availability based on scope and connection state |
| `data.poll_after_seconds` | integer | Recommended polling interval: 30 seconds |
| `data.action` | string | `WAIT`, `AUTHORIZE_ADMIN`, `CONTACT_SUPPORT`, `RECONNECT`, `WAIT_FOR_DELETION` |

| State or condition | `reason_code` | `action` |
|--------------------|---------------|----------|
| `ACTIVE`, all connection and consent checks pass | `null` | `null` |
| `ACTIVE`, proof expired, was invalidated or is missing | `ADMIN_CHECK_REQUIRED` | `AUTHORIZE_ADMIN` |
| `NEEDS_ADMIN` | `CONNECTION_ADMIN_REQUIRED` | `AUTHORIZE_ADMIN` |
| `SUSPENDED` | `CONNECTION_SUSPENDED` | `CONTACT_SUPPORT` |
| `DISCONNECTED` | `CONNECTION_DISCONNECTED` | `RECONNECT` |
| `DELETION_PENDING` | `CONNECTION_DELETION_PENDING` | `WAIT_FOR_DELETION` |

Polling does not renew administrator proof. `ACTIVE` alone does not open access. Use `bitrix_ready` and the separate `capabilities` values: the holder details do not affect Bitrix24, AI or storage availability. The status response includes only the account and consent-holder details documented above. It does not include people lists, internal identifiers, secrets or webhook URLs.

### Errors and polling retries

| HTTP | Code | Action |
|------|------|--------|
| 401 | `INVALID_API_KEY` | Check the current credential. Unknown, revoked, pre-rotation credentials and requests targeting another pair receive the same refusal |
| 403 | `CLIENT_BLOCKED` | The Product is blocked, contact support |
| 403 | `PORTAL_DELETED` | The account is deleted, stop polling |
| 403 | `PORTAL_BLOCKED` | The account is blocked, contact support |
| 403 | `PORTAL_SUSPENDED` | The account is suspended, contact support |
| 429 | `RATE_LIMITED` | IP limit reached. Wait at least `Retry-After` seconds |
| 429 | `CALLER_RATE_LIMITED` | Shared connection or account limit reached. Wait at least `Retry-After` seconds |

Example of the uniform refusal for an invalid credential:

```json
{ "success": false, "error": { "code": "INVALID_API_KEY", "message": "Invalid API key" } }
```

Poll no more often than once every `poll_after_seconds` seconds and increase the pause on 429. The baseline IP limit is 120 requests per minute. The effective limit is available in `x-ratelimit-limit`. Successful polling also consumes the shared connection and account limits alongside other API requests. Credential rotation does not reset these limits.

A `STATUS_ONLY` credential permits diagnostics but cannot call business methods. `REVOKED` credentials cannot even poll. Use the new credential after rotation. Protection for existing credentials and polling work regardless of new issuance availability. An account administrator reconnects through separate consent. The general codes from the table are also described in the [error reference](/docs/errors), while `CLIENT_BLOCKED` occurs only on this method.


### Who is connected

A successful COMPANY response from `POST /v1/connect/token` includes `portal` and
`user` at the top level. `GET /v1/connect/company/status` includes the same objects
under `data`. Names and email addresses come from platform account records, not
from the Bitrix24 profile method or a list of employees.

CREATE shows the consenting administrator. RECONNECT shows the administrator who
accepted the new consent. When another administrator performs ROTATE_SECRET,
`user` continues to identify the previous consent holder. Rotation does not
transfer consent. A missing email address is returned as an empty string.

`user` is `null` when current consent is absent, ended or from another generation;
when its holder is missing, blocked, deleted or has an `erasureState` other than
`ACTIVE`; or when the holder's membership is unavailable. An expired or invalidated
ADMIN check alone does not change identity. NEEDS_ADMIN without consent returns
the connected account and `user: null`.

The objects describe this response snapshot. A name or email change does not
necessarily increment `revision`. Token and status responses, including errors,
include `Cache-Control: no-store`. Do not cache profile details by revision.
These fields do not grant access to additional routes or renew ADMIN proof.
`api_key` is returned once on CREATE and ROTATE_SECRET and is absent on RECONNECT.
Token errors do not include `portal` or `user`.

Example of a successful COMPANY CREATE: `api_key` is delivered once; the secret below is fictional. `bitrix_ready: false` describes this connection's availability and does not enable production issuance.

```json
{
  "connection_mode": "COMPANY",
  "connection_id": "connection-id",
  "portal": { "domain": "example.bitrix24.com", "name": "Example" },
  "user": { "name": "Admin", "email": "admin@example.com" },
  "status": "ACTIVE",
  "generation": 1,
  "credential_version": 1,
  "credential_unchanged": false,
  "bitrix_ready": false,
  "scope": "crm",
  "api_key": "FAKE-example-only"
}
```

### Notifications and workday information

A Company credential acts in Bitrix24 on behalf of the administrator whose approval of the connection is currently in effect, with the approved scopes and write access. Bitrix24 still enforces that administrator's permissions for each employee and object.

With `im` consent, the credential can call `POST /v1/notifications` and read `GET /v1/notifications`, `/v1/notifications/search`, `/v1/notifications/schema`, `/v1/notifications/groups`, and `/v1/notifications/settings`. POST sends a personal notification, or a system notification with `type=system`, and requires the recipient `userId` and `message`; success returns 201. The list GET reads the current administrator's inbox, not a shared company inbox. Settings without `userId` belong to that administrator: the helper `user.current` additionally requires `user` consent. An explicit `userId` avoids that helper call; Bitrix24 determines access.

With `timeman` consent, the credential can read `GET /v1/workday/status`, `/v1/workday/settings`, `/v1/workday/records`, `/v1/workday/schedule`, `/v1/workday/time-control/settings`, `/v1/workday/time-control/report-settings`, `/v1/workday/time-control/users`, `/v1/workday/time-control/reports`, `/v1/workday/work-reports`, `/v1/workday/work-reports/subordinates`, `/v1/workday/work-reports/{id}`, `/v1/workday/daily-reports`, and `/v1/workday/records/{recordId}/report`.

Status and settings without `userId` refer to the current administrator. Records requires `userId` and one of `user_brief`, `user_basic`, or `user`, even for an empty page. Time-control/users requires `departmentId` and `department` consent because it also calls `department.get`. Schedule requires `id`; time-control/reports requires `userId`, `month`, and `year`; full/daily lists and an individual full report require `userId`. Subordinates refers to employees managed by the current administrator.

Consent is checked before every method, including helper calls. Missing consent returns 403 `SCOPE_MISSING`. To add scopes, update the application permissions and ask the administrator to approve them through RECONNECT. Company responses include `Cache-Control: no-store`. Other notification actions and workday changes remain unavailable to Company credentials and return 403 `CONNECT_COMPANY_ROUTE_FORBIDDEN`.

### Prepared Company handover protocol (issuance remains closed)

The existing authorization-code endpoints have a prepared Company branch. The production gate still returns `CONNECT_COMPANY_NOT_READY`; the following contract does not enable issuance.

`CONNECT_COMPANY_DISABLED` means that the connection is unavailable for the selected user or Bitrix24 account. `CONNECT_COMPANY_NOT_READY` means that temporary technical conditions for the connection have not yet been met. HTTP 429 `CONNECT_COMPANY_ATTEMPT_LIMITED` means too many attempts; retry after the indicated delay. These refusals do not require setting a daily cap.

Account and connection daily caps are independent and optional: `null` means no corresponding daily cap, not zero requests. Zero is not a valid cap. General request limits, attempt protection, usage accounting and billing remain in force. The absence of daily caps does not itself enable issuance.

All operations require fresh verification of Bitrix24 administrator rights, current consent, client authentication and PKCE S256. `RECONNECT` preserves the existing secret and does not address its possible theft. Use `ROTATE_SECRET` to replace the secret: it preserves the consented scopes, cannot expand them and remains available for an existing connection while new issuance is closed, provided the other checks pass.

Authorize accepts `operation=CREATE` (default), `RECONNECT`, or `ROTATE_SECRET`. Rotation also requires `credential_version` equal to the current version. All operations require confidential-client authentication and PKCE S256. For CREATE and RECONNECT, approval issues a code only after the restricted candidate webhook and encrypted cleanup obligation are saved. Rotation replaces the local secret without minting a webhook. Additional reconnect scopes require a separate explicit consent on the consent screen; the session approve request carries `approvedAdditionalScopes`.

- HTTP 409 `CONNECTION_GENERATION_CONFLICT`: another attempt won. No raw secret is returned; the losing candidate is scheduled for revocation. Start a new authorization.
- HTTP 409 `CONNECTION_WEBHOOK_CLEANUP_PENDING`: the limit of unresolved cleanups is reached. Wait for cleanup to finish.
- HTTP 403 `CONNECTION_SCOPE_CONSENT_REQUIRED`: new scopes have not been explicitly approved.
- HTTP 403 `CONNECTION_SUSPENDED`: security suspension cannot be lifted by ordinary reconnect.
- HTTP 503 `CONNECTION_ADMIN_CHECK_UNAVAILABLE`: administrator verification is inconclusive; the existing connection is preserved.

The first exchange and a successful rotation return the secret once. Reconnect keeps the secret unchanged. The platform deletes previous webhooks in the background, and this does not close Bitrix24 access: the connection works through the current holder's webhook while cleanup continues. A webhook that cannot be deleted stays recorded as a cleanup debt, and `webhook_cleanup` reports `pending` for it. If the outcome of creating a webhook is unknown, the cleanup obligation is kept. In that case, while the connection has no working webhook, Bitrix24 access is closed with reason `CONNECTION_WEBHOOK_CLEANUP_PENDING`. Automatic lookup of such a webhook by marker is not enabled. AI requires `vibe:ai` consent and a separate application permission; use `capabilities.ai` for current availability.

### Holder departure

A Company connection is held by the account administrator who completed consent. When that person stops being the holder, the connection moves to `NEEDS_ADMIN`. This happens when they are dismissed, deactivated or deleted in Bitrix24, become a guest, lose administrator rights or withdraw their consent, and also when their platform account is transferred or erased. The platform detects the loss of administrator rights during its scheduled re-check, not at the moment the change is made in Bitrix24.

After the holder departs, the status poll shows:

- `status` is `NEEDS_ADMIN`, `reason_code` is `CONNECTION_ADMIN_REQUIRED`, `action` is `AUTHORIZE_ADMIN`.
- `generation` and `revision` have each increased by 1.
- `webhook_cleanup` is `pending` while the platform deletes the previous holder's webhook in Bitrix24 or when it cannot be deleted, or `attention_required` if the outcome of creating the webhook is unknown.

The connection secret stays valid, and you do not need to change it. Unfinished authorization attempts are cancelled, and unused authorization codes can no longer be exchanged. The person's return to work does not restore the connection: an account administrator restores access by completing consent with `operation=RECONNECT`. Bitrix24 becomes available right after that, and cleanup of the previous holder's webhook continues in the background.

When the owner deactivates the application or the platform blocks it, all of its Company connections move to `SUSPENDED` and their secrets are revoked.

## When to use

This flow is for an external SaaS product that integrates with your clients' Bitrix24 accounts: CRM analytics, ERP synchronization, a chat assistant, a landing page builder. A "Connect Bitrix24" button on your site launches the flow: the user selects a Bitrix24 account and confirms the scopes; your server receives a permanent API key and works with the Bitrix24 account on behalf of the user.

If you are building an application that is installed **inside** a Bitrix24 account and works only with it, use a regular API key — see [Keys and authorization](/docs/keys-auth).

## How it works

```
Your application → [1. Redirect] → Vibecode Consent Page → [2. Consent]
    ↑                                                          ↓
    └──── [4. API key] ←── [3. Code → redirect_uri] ───────────┘
```

1. **Redirect** — you send the user to `/v1/connect/authorize` with `client_id`, `redirect_uri`, `state`, and a list of scopes.
2. **Consent** — the user sees a Vibecode page, selects a Bitrix24 account, and confirms or declines the requested scopes.
3. **Code** — after approval the user returns to `redirect_uri` with the `code` and `state` parameters. On decline — `redirect_uri?error=access_denied&state=...`.
4. **Exchange** — the partner's server sends `POST /v1/connect/token` and receives an API key.

## Endpoint reference

| Method | Path | Description |
|-------|------|----------|
| GET | [`/v1/connect/authorize`](#get-v1-connect-authorize) | Flow start: redirect to the consent page |
| POST | [`/v1/connect/token`](#post-v1-connect-token) | Exchange a one-time code for a permanent API key and poll a device sign-in |
| POST | [`/v1/connect/device/authorize`](#signing-in-from-a-device-without-a-browser) | Confirmation code for signing in from a device without a browser |
| POST | [`/v1/connect/revoke`](#revoking-a-key-from-the-application-post-v1-connect-revoke) | Revoke a single key from the application itself |
| GET | [`/v1/connect/company/status`](#company-connection-polling) | Diagnostics for your own Company connection |

## Full scenario

An example Express handler that walks the user through both endpoints end to end. Before running it, put the `client_id` and `client_secret` from the client registration into environment variables, and register the `redirect_uri` for that client.

```javascript
import express from 'express'
import crypto from 'node:crypto'

const app = express()
const sessions = new Map() // in production — Redis or a DB

const CLIENT_ID = process.env.PARTNER_CLIENT_ID
const CLIENT_SECRET = process.env.PARTNER_CLIENT_SECRET
const REDIRECT_URI = 'https://yourapp.com/callback'

// 1. The "Connect Bitrix24" button leads here
app.get('/connect', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex')
  sessions.set(state, { userId: req.user.id, createdAt: Date.now() })

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: 'crm task',
    state,
  })
  res.redirect(`https://vibecode.bitrix24.com/v1/connect/authorize?${params}`)
})

// 2. Vibecode returns the user here after consent or decline
app.get('/callback', async (req, res) => {
  const { code, state, error } = req.query

  const session = sessions.get(state)
  if (!session) return res.status(400).send('Unknown state')
  sessions.delete(state)

  if (error === 'access_denied') {
    return res.redirect('/dashboard?connect=denied')
  }

  // 3. Exchange the code for an API key — a server-side request with client_secret
  const tokenRes = await fetch('https://vibecode.bitrix24.com/v1/connect/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code,
      redirect_uri: REDIRECT_URI,
    }),
  })
  const data = await tokenRes.json()

  if (!tokenRes.ok) {
    // Errors arrive in RFC form: { error, error_description }
    return res.status(400).send(`Exchange error: ${data.error}`)
  }

  // 4. Store the binding: our service user ↔ Bitrix24 account
  await db.connections.upsert({
    userId: session.userId,
    portalDomain: data.portal.domain,
    portalName: data.portal.name,
    apiKey: encrypt(data.api_key), // secret, encrypt before storing
    scopes: data.scopes,
  })

  res.redirect('/dashboard?connect=ok')
})

// 5. From here on we use the stored key for calls on behalf of the user
async function listDeals(userId) {
  const connection = await db.connections.findByUserId(userId)
  const apiKey = decrypt(connection.apiKey)

  const res = await fetch('https://vibecode.bitrix24.com/v1/deals?limit=10', {
    headers: { 'X-Api-Key': apiKey },
  })
  return res.json()
}
```

Key points of the scenario:

- `state` is generated on the server and verified on return — without this check, an attacker could trick the user into using someone else's code.
- `client_secret` is kept only on the server and never reaches the browser.
- The API key is encrypted before being written to the DB and decrypted only at the moment of a call.
- On `error=access_denied` the user sees a clear message rather than an exchange error page.

## Connecting AI from an application inside Bitrix24

An application that opens in a frame inside a Bitrix24 account cannot show the consent page in that frame: the consent page blocks embedding in other sites with the `X-Frame-Options: SAMEORIGIN` and `Content-Security-Policy: frame-ancestors 'self'` headers. This way it cannot be spoofed or hidden under someone else's interface. So the "Connect AI" button opens the consent page in a separate browser window, and the frame waits for the result from your server.

The steps are:

1. The button in the frame opens a window at your server's address and passes a one-time attempt identifier, `ticket`, in the URL.
2. The server remembers the attempt and redirects the window to `/v1/connect/authorize` with the `vibe:ai` scope.
3. The user picks a Bitrix24 account and approves access. Vibecode returns the window to your `redirect_uri`.
4. The server exchanges the code for a key, marks the attempt as finished and closes the window.
5. The frame polls the server using the `ticket` and shows the result.

Code in the application frame:

```html
<button id="connect-ai" type="button">Connect AI</button>
<p id="connect-ai-status" role="status"></p>

<script>
  const button = document.getElementById('connect-ai')
  const statusLine = document.getElementById('connect-ai-status')

  button.addEventListener('click', () => {
    // Open the window right in the click handler: the browser blocks a window opened after await
    const ticket = crypto.randomUUID()
    const popup = window.open(
      `/connect/ai/start?ticket=${ticket}`,
      'vibe-connect-ai',
      'popup,width=640,height=760',
    )
    if (!popup) {
      statusLine.textContent = 'Allow pop-up windows for this application and press the button again'
      return
    }
    statusLine.textContent = 'Approve access in the window that opened'
    waitForResult(ticket)
  })

  async function waitForResult(ticket) {
    const deadline = Date.now() + 10 * 60 * 1000 // 10 minutes for consent
    while (Date.now() < deadline) {
      await new Promise((resolve) => setTimeout(resolve, 2000))
      const res = await fetch(`/connect/ai/status?ticket=${ticket}`)
      const result = await res.json()
      if (result.status === 'ok') {
        statusLine.textContent = 'AI connected'
        return
      }
      if (result.status === 'denied') {
        statusLine.textContent = `AI not connected: ${result.reason}`
        return
      }
    }
    statusLine.textContent = 'Connection not finished. Press the button again'
  }
</script>
```

The application server does the same code exchange as in the [full scenario](#full-scenario) and keeps the attempt state for the frame. `db` and `encrypt` are your storage and encryption. Register `https://yourapp.com/connect/ai/callback` with the client as a `redirect_uri`, and the `vibe:ai` scope must be among the client's scopes — see [Platform scopes](#platform-scopes).

```javascript
import express from 'express'
import crypto from 'node:crypto'

const app = express()
const attempts = new Map() // ticket → attempt result; use Redis or a database in production
const states = new Map() // state → ticket

const CLIENT_ID = process.env.PARTNER_CLIENT_ID
const CLIENT_SECRET = process.env.PARTNER_CLIENT_SECRET
const REDIRECT_URI = 'https://yourapp.com/connect/ai/callback'
const TICKET_RE = /^[0-9a-f-]{36}$/

// 1. The window opened by the button comes here and goes on to the consent page
app.get('/connect/ai/start', (req, res) => {
  const { ticket } = req.query
  if (typeof ticket !== 'string' || !TICKET_RE.test(ticket) || attempts.has(ticket)) {
    return res.status(400).send('Invalid connection attempt')
  }
  attempts.set(ticket, { status: 'pending' })

  const state = crypto.randomBytes(16).toString('hex')
  states.set(state, ticket)

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: 'vibe:ai',
    state,
  })
  res.redirect(`https://vibecode.bitrix24.com/v1/connect/authorize?${params}`)
})

// 2. Vibecode returns the window here after consent or refusal
app.get('/connect/ai/callback', async (req, res) => {
  const { code, state, error, error_description: reason } = req.query
  const ticket = states.get(state)
  if (!ticket) return res.status(400).send('Unknown state')
  states.delete(state)

  if (error) {
    // error_description carries the machine-readable reason code — see "Refusal after the consent screen"
    attempts.set(ticket, { status: 'denied', reason: reason ?? error })
    return res.send(closeWindowPage('Access was not granted. You can close this window'))
  }

  const tokenRes = await fetch('https://vibecode.bitrix24.com/v1/connect/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code,
      redirect_uri: REDIRECT_URI,
    }),
  })
  const data = await tokenRes.json()
  if (!tokenRes.ok) {
    attempts.set(ticket, { status: 'denied', reason: data.error })
    return res.send(closeWindowPage('The key was not received. You can close this window'))
  }

  await db.connections.upsert({
    portalDomain: data.portal.domain,
    apiKey: encrypt(data.api_key), // a secret, encrypt it before storing
    scopes: data.scopes,
  })
  attempts.set(ticket, { status: 'ok' })
  res.send(closeWindowPage('AI connected. You can close this window'))
})

// 3. The frame asks for the attempt result
app.get('/connect/ai/status', (req, res) => {
  res.json(attempts.get(req.query.ticket) ?? { status: 'unknown' })
})

function closeWindowPage(text) {
  return `<!doctype html><meta charset="utf-8"><p>${text}</p><script>window.close()</script>`
}
```

Key points:

- The window opens synchronously in the click handler. The browser blocks a window opened after `await` as a pop-up.
- The frame gets the result from its own server using the `ticket`, not from the window: the window goes to other sites, and polling your own server does not depend on what happens to the window along the way.
- `ticket` is not a secret: it only shows the attempt state. The key stays on the server and never reaches the frame.
- Polling is time-limited: the user may close the window without answering, and then nothing arrives at `redirect_uri`.
- A refusal arrives in `error` and `error_description`, and the frame shows the reason code as is. The codes are covered in [Refusal after the consent screen](#refusal-after-the-consent-screen).

## GET /v1/connect/authorize

Redirects the user to the Alaio Vibecode consent page.

**Query parameters:**

| Parameter | Type | Required | Description |
|----------|-----|:-----:|----------|
| `client_id` | string | yes | Partner identifier, received when you registered the client |
| `redirect_uri` | string | yes | Callback URL. Must match one of those registered for the client |
| `state` | string | yes | An arbitrary string that will be returned along with the code. CSRF protection |
| `scope` | string | no | Requested rights, space- or comma-separated: `crm task im`. If not passed, the rights registered for the client apply. A request for a right outside the registered set is rejected |
| `code_challenge` | string | conditional | SHA-256 hash of `code_verifier` in `base64url` without `=`. Required for public clients or when PKCE is mandatory, otherwise optional |
| `code_challenge_method` | string | conditional | `S256`, required together with `code_challenge`. Other methods, empty values and a method without a challenge are rejected |
| `portal` | string | no | Domain of the Bitrix24 account to preselect on the consent page: `yourcompany.bitrix24.com`. A hint, not a choice of account — see [Account hint](#account-hint) |

For a confidential client, PKCE is optional unless required in the client settings. If you send `code_challenge`, the platform stores it and requires the matching `code_verifier` when exchanging the code. PKCE supplements `client_secret`, rather than replacing it. Public clients and clients with mandatory PKCE always need a challenge.

**Example URL:**

```
https://vibecode.bitrix24.com/v1/connect/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&scope=crm%20task&state=abc123random
```

Server code that builds this link and checks `state` on return is in the [full scenario](#full-scenario). You can also build the link for your application without code: use the "Build a link" helper on the application card, see [Partner registration](#partner-registration).

**Successful redirect** after approval:

```
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=abc123random
```

**Redirect on decline** by the user on the consent page:

```
https://yourapp.com/callback?error=access_denied&state=abc123random
```

### Account hint

If your application knows which Bitrix24 account the user came from, pass that account's domain in the `portal` parameter. You don't need to strip the scheme or path, and case doesn't matter.

```
https://vibecode.bitrix24.com/v1/connect/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&scope=crm%20task&state=abc123random&portal=yourcompany.bitrix24.com
```

- If the user is an active member of that account, it is preselected on the consent page. Otherwise the account is selected the same way as without the parameter.
- The user can change the preselected account. The key is issued for the account the user confirmed, so read the key's account from the `portal.domain` field of the [`POST /v1/connect/token`](#post-v1-connect-token) response, not from your hint.
- The `authorize` response doesn't depend on the parameter: the application doesn't learn whether the user belongs to that account.
- An invalid value causes no error and is not reported to `redirect_uri` — sign-in proceeds as without the parameter.
- Only the authorization code flow for clients in `PERSONAL` mode uses the hint. Clients in `COMPANY` mode ignore the parameter, and [signing in from a device](#signing-in-from-a-device-without-a-browser) has no account hint.

### Errors

Errors from this endpoint arrive by three different routes, and all three need handling.

**Before `redirect_uri` is verified** — a plain `400` with an RFC 6749 body. There is nowhere to send the user back to: the address is not yet confirmed, and redirecting to an unverified URL would be a security hole.

| HTTP | `error` | Condition |
|------|---------|-----------|
| 400 | `invalid_request` | One of `client_id`, `redirect_uri`, `state` was not passed |
| 400 | `invalid_client` | `client_id` is unknown or the client is disabled |
| 400 | `invalid_request` | `redirect_uri` is malformed: it does not parse as a URL, or it carries a `#` fragment, a username or a password. `error_description` is `Redirect URI is malformed` |
| 400 | `invalid_request` | The `redirect_uri` scheme is not allowed for its host: not `https` on a non-localhost address, neither `http` nor `https` on localhost. `error_description` is `Redirect URI scheme is not allowed for this host` |
| 400 | `invalid_request` | `redirect_uri` matches none of the registered ones. `error_description` is `Redirect URI not registered` |

```json
{
  "error": "invalid_client",
  "error_description": "Unknown or inactive client"
}
```

**After `redirect_uri` is verified** — a redirect back to it carrying `error` and `state` (RFC 6749 §4.1.2.1). Your `redirect_uri` handler must parse this.

| `error` in the redirect | Condition |
|-------------------------|-----------|
| `invalid_scope` | A right outside the client's registered set was requested |
| `invalid_request` | Required PKCE is missing, or the supplied `code_challenge` or `code_challenge_method` is invalid. Only `S256` is supported |
| `access_denied` | The user pressed "Deny" on the consent page |
| `access_denied` | The user approved access, but no key could be issued: the Bitrix24 plan does not grant access to Vibecode, the region is unsupported, or the key limit has been reached. The machine-readable reason arrives in `error_description` |
| `temporarily_unavailable` | The user approved access, but key issuance did not finish because Bitrix24 was temporarily unavailable. The attempt can be repeated |

```
https://yourapp.com/callback?error=invalid_scope&state=abc123random
```

**Rate limited** — a `429` raised before any parameter is checked, so there is no redirect and no `state`. The limit is 30 requests per minute, counted for each `client_id` and caller-address pair. The current limit is returned in the `x-ratelimit-limit` header. The body arrives in the general API envelope — `{ "success": false, "error": { "code": "RATE_LIMITED", "message": "..." } }` — rather than in RFC 6749 form, and the `X-RateLimit-*` headers are present.

This route needs handling of its own because `authorize` is opened by a **browser navigation**: the user lands on a page of JSON instead of the consent screen, `redirect_uri` receives nothing, and your application never gets its `state` back. Once your own timeout expires, treat such an authorization as unfinished rather than waiting for a callback. The `Retry-After` value gives the minimum pause.

### Refusal after the consent screen

The refusal to issue a key comes after the user has already approved access. They see the reason on the Vibecode consent page and return to your application via a button — at which point `redirect_uri` receives `error`, `error_description` and the original `state`:

```
https://yourapp.com/callback?error=access_denied&error_description=INT_TARIFF_REQUIRED&state=abc123random
```

`error_description` carries the machine-readable reason, so the application can render its own screen:

| Code in `error_description` | What happened |
|-----------------------------|---------------|
| `INT_TARIFF_REQUIRED` | The account's Bitrix24 plan does not grant access to Vibecode. Where access requires a Vibe+ plan, `INT_VIBE_PLUS_REQUIRED` arrives instead |
| `KEY_LIMIT_REACHED` | A key cannot be issued: the user's key limit or the platform's per-account key cap has been reached — see [Key limit](#key-limit) |
| `BITRIX_UNAVAILABLE` | Bitrix24 did not respond while the key was being issued |
| `STATE_EXPIRED` | The consent link expired before confirmation — the authorization starts over from `/v1/connect/authorize` |
| `admin_approval_requested` | The user is not an administrator of the Bitrix24 account and asked the administrators to allow AI for the app — the request reached them in the bot chat. Arrives when the platform has enabled AI approval by account administrators. Once an administrator allows AI, the authorization starts over from `/v1/connect/authorize` |

The list of codes is open-ended: treat any `error_description` as a refusal reason instead of relying on the table above being exhaustive.

When access is refused for a payment-related reason, the user sees an inline payment card on the consent page. Its actions apply to the selected Bitrix24 account. For `INT_TARIFF_REQUIRED`, it offers to select a Bitrix24 plan; for `INT_VIBE_PLUS_REQUIRED`, a Vibe+ plan. Depending on the selected account, the `INT_TARIFF_REQUIRED` card may also offer Vibe+. If a Vibe+ trial is available for that account, the card offers to activate it. A Bitrix24 trial plan is activated in Bitrix24 itself, if one is available there.

Nothing reaches `redirect_uri` until the user chooses to return to your application. Returning after this refusal sends `error=access_denied`, the machine-readable reason in `error_description`, and the original `state`; no `code` or API key is issued. After a purchase or trial activation, return to your application and start authorization again from `/v1/connect/authorize`: the previous link's one-time state has already been consumed.

For the full list of general API errors, see [Errors](/docs/errors).

## POST /v1/connect/token

Exchanges a one-time authorization code for a permanent API key. Accepts `application/json` and `application/x-www-form-urlencoded`.

**Request fields (body):**

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `grant_type` | string | yes | `authorization_code` for this flow. Without it the endpoint responds with `unsupported_grant_type`. The same endpoint also serves `urn:ietf:params:oauth:grant-type:device_code` (see [Signing in from a device without a browser](#signing-in-from-a-device-without-a-browser)) |
| `client_id` | string | yes | Partner identifier |
| `client_secret` | string | yes | Partner secret. Pass only from the server side |
| `code` | string | yes | The code from the `code` parameter of the redirect |
| `redirect_uri` | string | yes | The same `redirect_uri` that was passed to `/v1/connect/authorize` |
| `code_verifier` | string | conditional | Original PKCE string. Required if `code_challenge` was sent during authorization, including for confidential clients |

### Examples

#### curl

```bash
curl -X POST https://vibecode.bitrix24.com/v1/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d code=RECEIVED_CODE \
  -d redirect_uri=https://yourapp.com/callback
```

#### JavaScript

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/connect/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET',
    code: receivedCode,
    redirect_uri: 'https://yourapp.com/callback',
  }),
})

const data = await res.json()
// data.api_key — permanent Vibecode API key
// data.portal — the Bitrix24 account selected by the user
// data.scopes — the requested scopes, data.granted_scopes — the key's rights
// data.user — the user who granted access
```

### Response fields

| Field | Type | Description |
|------|-----|----------|
| `api_key` | string | Vibecode API key in the `vibe_api_...` format. Pass it in the `X-Api-Key` header on subsequent calls |
| `portal.domain` | string | Bitrix24 account domain, for example `example.bitrix24.com` |
| `portal.name` | string | Bitrix24 account name |
| `scopes` | string[] | The rights the application requested. On the consent page the user confirms the set as a whole |
| `granted_scopes` | string[] | The rights the issued key actually carries. For a self-registered partner application they usually match `scopes` — they diverge if the key's rights were changed after consent |
| `user.name` | string | Name of the user who granted access |
| `user.email` | string | The user's email |

The two sets diverge only for applications whose scope set is assigned by the platform itself — the [Cowork](/docs/cowork) desktop application, for example. Such an application may request nothing at all, in which case `scopes` arrives empty while `granted_scopes` lists the key's full set. Check access against `granted_scopes`.

The `granted_scopes` field may be absent from the response if the platform could not read the issued key's rights. It never arrives empty, so a missing field means the set is unknown, not that there are no rights — in such a response, go by `scopes`.

The set in `granted_scopes` reflects what is written into the key. An individual right may not be confirmed on the Bitrix24 side, so diagnose a refusal by the Bitrix24 response code rather than by the presence of a right in the list.

### Response example

```json
{
  "api_key": "vibe_api_...",
  "portal": {
    "domain": "example.bitrix24.com",
    "name": "My company"
  },
  "scopes": ["crm", "task"],
  "granted_scopes": ["crm", "task"],
  "user": {
    "name": "John Smith",
    "email": "john@example.com"
  }
}
```

### Error response example

400 — a required parameter is missing:

```json
{
  "error": "invalid_request",
  "error_description": "Missing client_id, code or redirect_uri"
}
```

### Errors

The endpoint responds in RFC 6749 form — `{ error, error_description }`, with no `success` wrapper. This is deliberate, for compatibility with off-the-shelf OAuth libraries. The only exception is the refusal from the endpoint's own rate limiter, whose shape is described after the table.

| HTTP | `error` | Condition |
|------|---------|-----------|
| 400 | `invalid_request` | One of `client_id`, `code`, `redirect_uri` was not passed |
| 400 | `invalid_grant` | The code does not exist, has expired, has already been used, or `redirect_uri` differs from the one passed to `/v1/connect/authorize` |
| 400 | `invalid_grant` | The PKCE check failed: `code_verifier` is missing or does not match the `code_challenge` sent earlier |
| 400 | `invalid_client` | A public client sent a `client_secret` — a public client is issued no secret and must not send one |
| 400 | `unsupported_grant_type` | `grant_type` was not passed or is not one of the modes listed in the `grant_type` field above |
| 401 | `invalid_client` | `client_id` is unknown, the client is disabled, or `client_secret` does not match |
| 403 | `access_denied` | The client is in `COMPANY` mode, and Company key issuance is closed. `code` and `error_description` are `CONNECT_COMPANY_NOT_READY`; see [Connection mode](#connection-mode) |
| 400 | `unauthorized_client` | A device sign-in poll from a client in `COMPANY` mode. `code` and `error_description` are `CONNECT_COMPANY_DEVICE_FLOW_FORBIDDEN` |
| 429 | `slow_down` | The rate limit at the platform edge was exceeded. The limit is counted per caller address and is shared by the code exchange and the device poll. The response carries `Retry-After` with the minimum pause in seconds and carries no `X-RateLimit-*` headers |

**There are two rate limiters, and both answer `429`.** Tell them apart by the `X-RateLimit-Limit` header. The edge refusal from the table above does not carry that header and arrives in RFC 6749 form. The endpoint's own refusal does carry it, and its body arrives in the general API envelope — `{ "success": false, "error": { "code": "RATE_LIMITED", "message": "..." } }`. The action is the same in both cases: wait and retry with a longer pause.

Refusals of the prepared Company handover arrive with HTTP 403, 409 and 503 and a `code` field; they are listed in [Prepared Company handover protocol](#prepared-company-handover-protocol-issuance-remains-closed).

For the full list of general API errors, see [Errors](/docs/errors).

## Public client: the PKCE flow

Everything above describes a confidential client — one that has a server and a `client_secret`. If there is no server and nowhere to hide a secret (a mobile application, a desktop program, a single-page web application), register a public client. It is issued no secret; authenticity is proven by PKCE instead.

Three things differ from the main flow; everything else is identical.

**1. The application generates a pair on every run of the flow.** A random string (`code_verifier`) and its SHA-256 hash in base64url without the `=` padding (`code_challenge`).

```javascript
import crypto from 'node:crypto'

const b64url = (buf) => buf.toString('base64url')
const verifier  = b64url(crypto.randomBytes(32))
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest())
```

Keep `verifier` until the flow ends; `challenge` goes into the first request.

**2. Two parameters are added to `/v1/connect/authorize`.**

| Parameter | Value |
|-----------|-------|
| `code_challenge` | The hash from step 1 |
| `code_challenge_method` | Always `S256`. Other values are rejected |

For a public client they are **mandatory**: without them the flow ends in a `?error=invalid_request` redirect.

**3. `/v1/connect/token` takes `code_verifier` instead of `client_secret`.**

```bash
curl -X POST https://vibecode.bitrix24.com/v1/connect/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "YOUR_CLIENT_ID",
    "code": "RECEIVED_CODE",
    "redirect_uri": "http://127.0.0.1:9999/callback",
    "code_verifier": "CODE_VERIFIER"
  }'
```

The response is the same as for a confidential client:

```json
{
  "api_key": "vibe_api_...",
  "scopes": ["crm"],
  "granted_scopes": ["crm"],
  "portal": { "domain": "example.bitrix24.com", "name": "My company" },
  "user": { "name": "John Smith", "email": "john@example.com" }
}
```

**A public client must not send `client_secret`** — the platform responds with `400 invalid_client`. This is not pedantry: a secret shipped inside a distributed application is not a secret, and the flow is built so that such a secret cannot be used.

### Returning to localhost

A program with no site of its own has no public address to receive the code at. For such cases a loopback redirect is allowed: register a `redirect_uri` such as `http://127.0.0.1:9999/callback`, start a temporary listener on that port for the duration of the flow, and close it as soon as the code arrives.

`127.0.0.1`, `localhost` and `[::1]` are accepted, with either `http` or `https`. Every other address must be `https`. The scheme and the path are always matched; whether the port and the localhost address itself are matched depends on the client type:

| Client type | How a localhost address is matched |
|---|---|
| Public | The port is **not** part of the match — if it is taken, the application may listen on any other one without re-registering the address. The addresses `127.0.0.1`, `localhost` and `[::1]` are interchangeable: a registered `http://127.0.0.1/callback` also accepts `http://localhost:5123/callback` |
| Confidential | The whole address is matched, port included. This is how a server application is debugged on your own machine: register the address with the port your server listens on, for example `http://localhost:3030/callback` |

The name `localhost` can point somewhere other than this machine if the network settings override it, so `127.0.0.1` is the more reliable choice.

## Signing in from a device without a browser

A TV, a set-top box, a terminal utility — anywhere typing a login and password is awkward. The user confirms the sign-in on a phone or computer, while the device receives access. The flow is defined by RFC 8628.

The device needs device mode enabled: a platform administrator grants it, and only to verified applications. A self-registered client gets `400 unauthorized_client` — this protects users from having a confirmation code shown to them by an unverified application.

**Step 1. The device requests a code.**

```bash
curl -X POST https://vibecode.bitrix24.com/v1/connect/device/authorize \
  -H "Content-Type: application/json" \
  -d '{"client_id":"YOUR_CLIENT_ID","scope":"crm","code_challenge":"HASH","code_challenge_method":"S256"}'
```

```json
{
  "device_code": "MYQnuWB3H0tR...",
  "user_code": "54BV-XT8F",
  "verification_uri": "https://vibecode.bitrix24.com/connect/device",
  "verification_uri_complete": "https://vibecode.bitrix24.com/connect/device?user_code=54BV-XT8F",
  "expires_in": 899,
  "interval": 5
}
```

`device_code` is the device secret and is never shown on screen. `user_code` is displayed to the user together with the address. `expires_in` is how many seconds the user has to confirm.

The request accepts `application/json` and `application/x-www-form-urlencoded`.

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `client_id` | string | yes | Identifier of an application for which a platform administrator has enabled device sign-in |
| `scope` | string | no | Requested scopes, separated by spaces or commas. If omitted, the scopes registered for the client apply |
| `code_challenge` | string | conditional | SHA-256 hash of `code_verifier` in `base64url` without `=`. Required if PKCE is enabled in the client settings. The platform stores a supplied hash and checks it against `code_verifier` during polling |
| `code_challenge_method` | string | conditional | `S256`, required together with `code_challenge` |
| `device_id` | string | no | A stable identifier of the application installation: up to 128 characters, Latin letters, digits and the characters `.`, `:`, `-`, `_`. The platform adds it to `verification_uri_complete` as the `device_id` parameter so the steps of one sign-in stay linked even if the link is opened in another browser. A value of any other shape is dropped without an error: the response stays `200` and the link arrives without the parameter. It does not affect key issuance for a third-party application |
| `login_source` | string | no | Your analytics label for the sign-in button: a lowercase Latin letter followed by lowercase Latin letters, digits and `_`, up to 64 characters in total. Surrounding spaces are trimmed. An invalid label does not interrupt sign-in and does not affect permissions or key issuance |

Refusals of this step arrive in RFC 6749 form — `{ error, error_description }`:

| HTTP | `error` | Condition |
|------|---------|---------|
| 400 | `invalid_request` | `client_id` was not passed. `error_description` is `Missing client_id` |
| 401 | `invalid_client` | `client_id` is unknown or the client is disabled |
| 400 | `unauthorized_client` | Device sign-in is not enabled for this client. `error_description` is `Client is not enabled for the device flow` |
| 400 | `unauthorized_client` | The client is in `COMPANY` mode. `code` and `error_description` are `CONNECT_COMPANY_DEVICE_FLOW_FORBIDDEN` |
| 400 | `invalid_request` | The client requires PKCE and `code_challenge` was not passed, or the method is not `S256` or the hash is malformed |
| 400 | `invalid_scope` | An unknown scope was requested, or a scope outside the client's registered set |

This step is rate limited too — 30 requests per minute by caller address, with `client_id` not part of the rate-limit key. On top of that the platform-edge limiter applies, shared by the whole of `/v1/`. Exceeding either returns `429` in the general API envelope with a `Retry-After` header; the presence of `X-RateLimit-Limit` shows which one fired.

**Step 2. The user opens the address and confirms.** The confirmation screen shows the application card, the requested rights, the Bitrix24 account picker, and a phishing warning: confirm only a sign-in you started yourself.

**Step 3. The device polls `/v1/connect/token`.**

```bash
curl -X POST https://vibecode.bitrix24.com/v1/connect/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "client_id": "YOUR_CLIENT_ID",
    "device_code": "DEVICE_CODE",
    "code_verifier": "CODE_VERIFIER"
  }'
```

Until the user confirms, the answer is `400` with one of these states:

| `error` | What to do |
|---------|-----------|
| `authorization_pending` | Wait and keep polling at the `interval` cadence |
| `slow_down` | Polling is faster than allowed. Add 5 seconds to the interval and continue |
| `access_denied` | The user declined. Stop polling |
| `access_denied` with `error_description=admin_approval_requested` | The user asked an administrator of the Bitrix24 account to allow AI for the app. Arrives when the platform has enabled AI approval by account administrators. Stop polling and, after the administrator decides, request a new code from step 1 |
| `expired_token` | The code expired. Request a new one from step 1 |

A poll can also receive `429` — that is a rate-limiter refusal, not an authorization state, and it **comes in two forms**, because this route has two limiters. The platform-edge refusal arrives in RFC 6749 form (`error` set to `slow_down`) and carries no `X-RateLimit-*` headers. The endpoint's own limiter refuses in the general API envelope — `{ "success": false, "error": { "code": "RATE_LIMITED", "message": "..." } }` — and does carry `X-RateLimit-*`. A branch on `error === "slow_down"` alone is therefore not enough: it misses half the refusals.

Handle both the same way: wait at least as long as `Retry-After` says and keep polling, without resetting your own growing pause. If you need to tell them apart, check for `X-RateLimit-Limit`.

After confirmation the same request returns `200` with the key and two scope sets:

```json
{ "api_key": "vibe_api_...", "scopes": ["crm"], "granted_scopes": ["crm"] }
```

Note that this object is **narrower** than in the main flow — no `portal` and no `user` blocks.

The key is delivered **once**. A repeat poll with the same `device_code` returns `expired_token`, so store the key immediately.

### What the device must handle

- **Show a countdown.** The code lives for 15 minutes; when it runs out, show a new one rather than a dead screen.
- **Respect `interval` and react to `slow_down`.** Polling faster than allowed does not speed up delivery — it produces refusals.
- **Read `Retry-After` and grow your own pause.** The header value is a minimum pause, not a guarantee that the next request will be accepted: every running copy of the application competes for the same per-address limit. Wait at least as long as `Retry-After` says, and still increase your own pause on every refusal.
- **Never display `device_code`.** Only `user_code` goes on screen.

## Using the API key

The received key is of type `APP` and works like a standard Vibecode API key. Pass it in the `X-Api-Key` header:

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

The same key can be used with any Vibecode API endpoint within the confirmed scopes: deal lists, batch requests, aggregation, and so on.

A partner key carries exactly the permissions the user confirmed on the consent page: platform scopes are never added to the set on their own. If you need the AI Router (`/v1/chat/completions`), request `vibe:ai` along with the rest ([here is how](#platform-scopes)). Skip it and the call returns `403`. Web search (`/v1/search`), `vibe:infra` and `vibe:feedback` are granted by a platform administrator, and `vibe:storage` is not available to a Connect application — the same section covers both.

When the platform has enabled AI approval by account administrators, an administrator of the Bitrix24 account grants AI access to an app — on the consent page or at an employee's request. Until it is enabled, any employee who confirms access grants AI access to the app, and the restrictions in this paragraph do not apply. If the app requests `vibe:ai`, the user confirming consent is not an administrator, and AI access has not been granted to the app on that account, the consent page refuses with `CONNECT_AI_ADMIN_REQUIRED` and offers to ask an administrator; after the request, `redirect_uri` receives `error=access_denied` with `error_description=admin_approval_requested`. Until AI access is granted, or after an administrator revokes it, the model routes (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models` and others) return `403` with the code `CONNECT_AI_NOT_PERMITTED`. The key's other permissions keep working and the key does not need to be reissued: once an administrator grants AI access, the same key can access the models again.

The partner key works with a single `X-Api-Key` header and does **not** require `Authorization: Bearer <session_token>`. The prefix tells the two apart at a glance: a partner key starts with `vibe_api_`, an OAuth application key from the Vibecode catalog with `vibe_app_`, and the latter does require a user session.

## Available scopes

When calling `/v1/connect/authorize`, pass the same scopes you would when creating a standard API key in a Bitrix24 account. On the consent page the user sees the list of requested rights and confirms it as a whole — an individual item cannot be removed; only "Allow" and "Deny" are available. The final set arrives in the `granted_scopes` field of the `/v1/connect/token` response, with the requested one in the neighbouring `scopes` field.

The current list of supported values is described in the [Keys and authorization](/docs/keys-auth) section — it grows as new Bitrix24 modules appear, so it is not duplicated here.

The Vibecode platform scopes fall into three groups: an application requests `vibe:ai` itself; `vibe:search`, `vibe:infra` and `vibe:feedback` are granted by a platform administrator; and `vibe:storage` is not available to a Connect application. The procedure is in [Platform scopes](#platform-scopes).

A machine-readable list of what an application may request on its own is served in the `scopes_supported` field of the `/.well-known/oauth-authorization-server` document (RFC 8414). Scopes an administrator granted to one specific application never appear in that anonymous document.

## Key lifetime and revocation

- **The authorization code (`code`)** is valid for **5 minutes** and is single-use. After the exchange via `/v1/connect/token`, a repeat call with the same code returns `invalid_grant`.
- **The consent state** (the internal state of the consent page) lives for 10 minutes — if the user does not confirm within that time, the link has to be issued again.
- **The API key** has no expiration. It works until it is revoked. Revocation happens in one of four ways:

  1. **The user revokes access themselves** — the "Connected apps" section of their Vibecode profile. That revokes **every** key they issued to your application for that Bitrix24 account, including keys from earlier authorizations. Access granted by other employees on the same account is not affected.
  2. **The application revokes its own key** — `POST /v1/connect/revoke`, see below. Exactly one key is revoked: the one you presented.
  3. **A platform administrator** revokes the keys the client has issued.
  4. **The app owner permanently deletes the client** — in the [Connect dashboard](/connect-apps), choose “Deactivate”, then open the inactive app’s menu, choose “Delete permanently” and confirm. All keys issued by that client will stop working, and its short address will become available. The deleted client cannot be restored. Owners cannot permanently delete official apps.

  **Deactivating a client does NOT revoke keys** — it only stops new issuance: with an inactive client `/v1/connect/authorize` rejects the request, while keys already issued keep working.

When a key is revoked, all requests with it return `401 KEY_INACTIVE`. If the key is deleted or unknown, the response is `401 INVALID_API_KEY`. The partner must handle both codes correctly and prompt the user to re-authorize.

### Revoking a key from the application: `POST /v1/connect/revoke`

The endpoint follows RFC 7009 and revokes **one** key — the one you present in it. Call it when the user disables your integration on your side: otherwise the earlier keys keep working and keep occupying slots in the key limit (see below).

```
POST /v1/connect/revoke
Content-Type: application/x-www-form-urlencoded

client_id=your-app&client_secret=your-secret&token=<api_key>
```

JSON with the same fields is accepted too. Public (`PUBLIC`) clients send no `client_secret` — they have none; confidential clients must send it.

Responses:

- `200 {}` — either the key was revoked, or there was nothing to revoke. An unknown token, a key issued to another application and an already revoked key all get the same answer, so you cannot probe whether someone else's keys are alive. That also makes the call **idempotent**: repeating it is safe.
- `400 invalid_request` — `client_id` or `token` is missing.
- `400 invalid_client` — a public client presented a `client_secret`.
- `401 invalid_client` — unknown client, or the secret did not match.
- `409 KEY_MANAGES_FOREIGN_SERVER` — the key manages a live server owned by another user, a server without an owner, or a server owned by the same user in another Bitrix24 account. Set the server's owner and its Bitrix24 account, rebind it to a key owned by that user in that account, then retry revocation.
- `409 KEY_OPERATION_IN_PROGRESS` — another operation is changing this key. Retry when it finishes. Both refusal bodies contain `error`, `error_description` and `code`.
- `429` — rate limited, and there are **two** limiters here. The endpoint's own limiter: 60 requests per minute. The current limit is returned in the `x-ratelimit-limit` header. Counted **by caller address alone** — `client_id` is deliberately not part of the rate-limit key, so rotating it yields no fresh budget, and every copy of an integration behind one address shares those 60; it does set `X-RateLimit-*` headers. The platform-edge limiter: 60 requests per **second** per address, counted at the edge — a bucket shared by the whole of `/v1/` rather than by this endpoint; it sets no `X-RateLimit-*` headers and its `Retry-After` is always 1. In both cases the body arrives in the general API envelope — `{ "success": false, "error": { "code": "RATE_LIMITED", "message": "..." } }` — which differs from the RFC 7009 shape of this endpoint's other answers, so the body cannot tell the two apart, only the presence of `X-RateLimit-Limit` can. The call is **not** terminal: the key was not revoked, so repeat it after the pause named in `Retry-After`.

A key issued before your client was deactivated can be revoked as well: deactivation stops new issuance but does not block revoking the old keys.

What the endpoint does not do:

- **it does not remove the webhook on the Bitrix24 account** — the user deletes that on their side, and revoking a key is no substitute;
- **it does not bring a lost key back** — to revoke a key you have to present it, and it cannot be recovered;
- **it does not fire by itself** — if the user simply stops using the integration, the key lives on; calling the endpoint is your responsibility.

If the presented key was issued to your client and was active, a successful revocation stops it immediately: requests with it return `401 KEY_INACTIVE`, and its slot in the limit is freed. A `200 {}` response alone does not prove revocation: an unknown key, a key issued to another client, and an already revoked key receive the same response.

### Re-authorization issues a new key

Every pass through the consent screen issues a **new** API key. The previous key for the same user and Bitrix24 account keeps working: authorization does not revoke it. The user's profile shows both issuances as one connection, and revoking access invalidates them together.

What this means for your application:

- store the key you received last and replace the previous one with it, otherwise you accumulate working keys you no longer track,
- every re-authorization takes a slot in the key limit — see below,
- a key that has lost its validity cannot be restored: obtain a new one through `/v1/connect/authorize`.

### Key limit

One user on one Bitrix24 account holds at most 100 valid keys — this is both the default and the highest value the setting accepts. A Bitrix24 account administrator can set another value, from 1 to 100. The count covers every key the user holds on that account, not only the ones your application issued.

In addition to the per-user limit, there is the platform's per-account key cap. It is counted across all live keys on the account and cannot be raised by a Bitrix24 account administrator. Once either limit is reached, issuance is refused with the code `KEY_LIMIT_REACHED`: the user sees the reason on the consent page, while the application receives only that code in `error_description` on the return to `redirect_uri`. For the per-user limit, a slot is freed by deleting unneeded keys in the ["API Keys"](/keys) section of the Vibecode dashboard, or by revoking the application's access in the profile. For the per-account cap, the consent screen asks the user to choose a plan with a higher key limit. Deleting keys also frees capacity once the total number of valid keys on the account falls below the cap.

## Managing apps in a Bitrix24 account

An administrator of the selected Bitrix24 account sees every employee's apps in the profile, on the "All apps in the account" tab: connections, AI requests and each app's share of the quota. The "AI Models" page adds a "By app" breakdown.

"Allow AI" approves a pending request or grants permission directly. Permission does not create a key: the employee has to connect again. "Block AI" stops model calls while the key keeps its other scopes. "Disconnect for everyone" revokes every key of the app in this Bitrix24 account and the tokens derived from them. Usage for the current period stays in the reports.

These controls are available when the platform has enabled AI approval by account administrators. Keys in other Bitrix24 accounts are not affected.

## Security

- **State parameter** — generate a cryptographically strong random string for each request and verify it on return to `redirect_uri`. Protects against CSRF.
- **`client_secret` — server only.** Never pass the secret to a browser, mobile app, or client-side code. The code-for-key exchange is performed only by a server-side request.
- **Key storage.** The API key is a secret that grants access to the user's data. Store it encrypted, restrict access to the stored connection record, do not log it.
- **`redirect_uri` binding.** The value in `/v1/connect/authorize` and `/v1/connect/token` must match character for character — a mismatch results in `invalid_grant`.

## Partner registration

You register the client yourself in the Vibecode dashboard — the [Connect apps](/connect-apps) section in the sidebar, under the "Access" group. Sign in with your Vibecode account and click "Register app" — the `client_id` is issued immediately, with no request to file and no upfront review.

The form asks for:

- the name and a short description of the application,
- an identifier — lowercase Latin letters, digits and hyphens,
- one or more `redirect_uri` values — `https://` addresses or the local `127.0.0.1`, `localhost` and `[::1]` over `http` or `https`,
- the access rights you request.

The set of rights cannot be empty: registration without a single right returns `EMPTY_SCOPES`. An application with no rights cannot request anything from the user.

The client type is chosen at creation and cannot be changed later — see [Client types](#client-types) for a walkthrough with examples.

The `client_secret` is shown once, at creation. You cannot read it again, only rotate it with the "Rotate secret" action.

The ⋮ menu on the application card carries two helpers: **"Build a link"** — an authorization-link builder (pick the `redirect_uri` and the scopes, get a ready URL and the `curl` for the code exchange; for a client without a secret, a one-time PKCE pair of strings), and **"Client's view"** — a preview of the consent page exactly as the user will see it, verification badge included.

The number of active clients per user is limited. Once the limit is reached, registration returns `CLIENT_LIMIT_REACHED` and the current value arrives in the `limit` field. Deactivating a client frees a slot.

A new application gets the "unverified" status — the user sees that mark on the consent page. A platform administrator can raise the status, and the request is sent with the "Request verification" action on the application card. A request can be declined with a reason — the reason reaches the owner of the application, and once the fixes are made, the request is submitted again.

### Platform scopes

Vibecode scopes grant access to sections of the platform itself rather than to Bitrix24 data. For a Connect application they fall into three groups.

**An application requests `vibe:ai` itself.** Tick it among the scopes at registration — in the same place as the Bitrix24 scopes — and from there it travels the usual path: it is passed in the `scope` parameter of `/v1/connect/authorize`, shown to the user on the consent page as an "AI models" line, and lands on the issued key. No request form and no review are needed for this. Consumption is covered by the monthly AI quota of the account the application connects to.

The scope is never added automatically: an application that did not tick it works without AI Router access, and previously issued keys keep their scope sets. To enable AI for an existing connection, tick the scope on the application card and walk the user through consent again — they will receive a new key that carries it.

**`vibe:search`, `vibe:infra` and `vibe:feedback` are granted by a platform administrator.** At self-registration a request carrying them is rejected with `SCOPE_NOT_ALLOWED`. The procedure takes three steps, and the owner of the application performs the first two:

1. Submit a request with the "Request verification" action on the application card.
2. In the "Discussion with the platform" thread on that same card, write which scope you need and for which scenario. The request carries no dedicated "requesting this scope" field, so without a message the reviewer only sees a request for the badge.
3. The administrator grants the scope to a verified application. Verification on its own grants no scopes — these are two separate decisions, and the first one is a precondition for the second.

Once granted, the scope behaves like `vibe:ai`: passed in `scope`, shown on the consent page, landing on the key.

**`vibe:storage` is not available to a Connect application.** A key issued through Connect lands in the same [Storage](/docs/storage) area as the personal keys of the connecting user in that Bitrix24 account. With this scope, the application would see every file uploaded with that user's personal keys. For this reason, not even a platform administrator grants this scope, and a request carrying it at self-registration is rejected with `SCOPE_NOT_ALLOWED`.

### What removes the "verified" mark

The mark rests on the shape the application had when it passed review, so editing any of these five fields returns it to "unverified":

- the name,
- the description,
- the logo,
- the `redirect_uri` return addresses,
- the set of rights you request.

The site address does not affect the mark. Saving the same description again does not remove the mark; a missing description and an empty string are equivalent.

The dashboard warns you before saving: the form shows a confirmation listing the fields that will remove the mark. Without that confirmation the edit is not saved, and the response is `VERIFICATION_RESET_NOT_CONFIRMED` carrying the same list in its body.

Populating a previously empty set of rights does not remove the mark when the chosen rights fit inside the set granted by the administrator.

The reset touches the mark alone. Issued keys, connected users, platform scopes and an already-granted device-code sign-in all stay in place. Submit the verification request again.

## Client types

The type answers one question: does the application have a place the user cannot reach, where a secret can live? The answer decides whether a `client_secret` is issued.

### Confidential

The application has a server side. The `client_secret` lives there and never reaches the browser or the user's device.

Examples: a SaaS analytics service that visits the customer's Bitrix24 account on a schedule. An ERP sync running on your own server. A chat assistant that receives webhooks and answers from its backend.

This is the type the flow on this page needs: the code is exchanged for a key by a server-side request carrying the `client_secret`.

### Public

There is nowhere to hide a secret — the code runs entirely on the user's device. No `client_secret` is issued. Authenticity is proven by PKCE instead: on every run of the flow the application generates a random string, sends its hash to `/v1/connect/authorize`, and the string itself to `/v1/connect/token`. An intercepted code cannot be exchanged without it.

Examples: a mobile application. A desktop program. A single-page web application with no backend of its own.

### How to choose

If the application has a server you control, pick confidential. If all the code ships to the user, pick public.

The type is set once, at registration. To change it, register a new client.

## See also

- [Keys and authorization](/docs/keys-auth)
- [Management keys](/docs/management-keys)
- [API errors](/docs/errors)
