لوكلاء AI: ملف markdown لهذه الصفحة — /docs-content-en/partner-connect.md فهرس التوثيق — /llms.txt
مقالات التوثيق متاحة حاليًا باللغة الإنجليزية فقط.
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
- Company connection polling
- When to use
- How it works
- Endpoint reference
- Full scenario — an Express handler end to end
- Connecting AI from an application inside Bitrix24 — a button and consent in a separate window
GET /v1/connect/authorize— flow start, redirect to consentPOST /v1/connect/token— exchanging the code for an API key- Public client: the PKCE flow — an application with no server
- Signing in from a device without a browser — TV, set-top box, terminal
- Using the API key
- Available scopes
- Key lifetime and revocation
- Security
- Partner registration
- Client types — public or confidential, with examples
- See also
Connection mode
New clients use PERSONAL: consent and the key belong to the user. COMPANY prepares a shared company connection to a Bitrix24 cloud 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.
Issuance for COMPANY is currently closed. GET /v1/connect/authorize redirects to the registered redirect_uri with error=access_denied, the original state and error_description=CONNECT_COMPANY_NOT_READY regardless of preparation availability. 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 same code in code and error_description.
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 cannot access storage: HTTP 403 CONNECT_COMPANY_STORAGE_NOT_READY. 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. Production issuance remains closed regardless of the rollout flag.
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
curl --max-time 20 "$VIBE_URL/v1/connect/company/status" \
-H "X-Api-Key: $VIBE_COMPANY_SECRET"
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:
{
"success": true,
"data": {
"connection_id": "connection-id",
"connection_mode": "COMPANY",
"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 |
|---|---|---|
Fieldsuccess |
Typeboolean | Meaningtrue for successful polling |
Fielddata |
Typeobject | MeaningDiagnostics for your own connection |
Fielddata.connection_id |
Typestring | MeaningConnection identifier returned by this method |
Fielddata.connection_mode |
Typestring | MeaningAlways COMPANY |
Fielddata.status |
Typestring | MeaningACTIVE, NEEDS_ADMIN, SUSPENDED, DISCONNECTED, DELETION_PENDING |
Fielddata.generation |
Typeinteger | MeaningConnection consent generation |
Fielddata.revision |
Typeinteger | MeaningStored connection state revision |
Fielddata.credential_version |
Typeinteger | MeaningCurrent connection credential version |
Fielddata.bitrix_ready |
Typeboolean | MeaningCurrently always false |
Fielddata.reason_code |
Typestring | MeaningReason access is closed, see the table below |
Fielddata.webhook_cleanup |
Typestring | Meaningnone: no unfinished attempts, pending: cleanup or creation is unfinished, attention_required: webhook creation outcome is unknown |
Fielddata.capabilities.bitrix |
Typeboolean | MeaningCurrently always false |
Fielddata.capabilities.ai |
Typeboolean | MeaningCurrently always false |
Fielddata.capabilities.storage |
Typeboolean | MeaningCurrently always false |
Fielddata.poll_after_seconds |
Typeinteger | MeaningRecommended polling interval: 30 seconds |
Fielddata.action |
Typestring | MeaningWAIT, AUTHORIZE_ADMIN, CONTACT_SUPPORT, RECONNECT, WAIT_FOR_DELETION |
| State or condition | reason_code |
action |
|---|---|---|
State or conditionACTIVE, administrator proof is valid |
reason_codeCONNECT_COMPANY_NOT_READY |
actionWAIT |
State or conditionACTIVE, proof expired, was invalidated or is missing |
reason_codeADMIN_CHECK_REQUIRED |
actionAUTHORIZE_ADMIN |
State or conditionNEEDS_ADMIN |
reason_codeCONNECTION_ADMIN_REQUIRED |
actionAUTHORIZE_ADMIN |
State or conditionSUSPENDED |
reason_codeCONNECTION_SUSPENDED |
actionCONTACT_SUPPORT |
State or conditionDISCONNECTED |
reason_codeCONNECTION_DISCONNECTED |
actionRECONNECT |
State or conditionDELETION_PENDING |
reason_codeCONNECTION_DELETION_PENDING |
actionWAIT_FOR_DELETION |
Polling does not renew administrator proof. ACTIVE alone does not open access: Bitrix24 and AI currently return CONNECT_COMPANY_NOT_READY, files return CONNECT_COMPANY_STORAGE_NOT_READY. The response contains no personal data, people lists, secrets or URLs.
Errors and polling retries
| HTTP | Code | Action |
|---|---|---|
| HTTP401 | CodeINVALID_API_KEY |
ActionCheck the current credential. Unknown, revoked, pre-rotation credentials and requests targeting another pair receive the same refusal |
| HTTP403 | CodeCLIENT_BLOCKED |
ActionThe Product is blocked, contact support |
| HTTP403 | CodePORTAL_DELETED |
ActionThe account is deleted, stop polling |
| HTTP403 | CodePORTAL_BLOCKED |
ActionThe account is blocked, contact support |
| HTTP403 | CodePORTAL_SUSPENDED |
ActionThe account is suspended, contact support |
| HTTP429 | CodeRATE_LIMITED |
ActionIP limit reached. Wait at least Retry-After seconds |
| HTTP429 | CodeCALLER_RATE_LIMITED |
ActionShared connection or account limit reached. Wait at least Retry-After seconds |
Example of the uniform refusal for an invalid credential:
{ "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, while CLIENT_BLOCKED occurs only on this method.
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.
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. 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 cleanup cap is reached. Wait for cleanup before reconnecting. - 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. Bitrix24 use remains blocked until terminal proof that previous webhooks were deleted. An uncertain mint keeps a durable cleanup obligation; automatic lookup by marker is not enabled. Company AI consent is not enabled by these flows.
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 a lost administrator right during its scheduled re-check, not at the moment the change is made in Bitrix24.
After the holder departs, the status poll shows:
statusisNEEDS_ADMIN,reason_codeisCONNECTION_ADMIN_REQUIRED,actionisAUTHORIZE_ADMIN.generationandrevisionhave each increased by 1.webhook_cleanupispendingwhile the platform deletes the previous holder's webhook in Bitrix24, orattention_requiredif 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.
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.
How it works
Your application → [1. Redirect] → Vibecode Consent Page → [2. Consent]
↑ ↓
└──── [4. API key] ←── [3. Code → redirect_uri] ───────────┘
- Redirect — you send the user to
/v1/connect/authorizewithclient_id,redirect_uri,state, and a list of scopes. - Consent — the user sees a Vibecode page, selects a Bitrix24 account, and confirms or declines the requested scopes.
- Code — after approval the user returns to
redirect_uriwith thecodeandstateparameters. On decline —redirect_uri?error=access_denied&state=.... - Exchange — the partner's server sends
POST /v1/connect/tokenand receives an API key.
Endpoint reference
| Method | Path | Description |
|---|---|---|
| MethodGET | Path/v1/connect/authorize |
DescriptionFlow start: redirect to the consent page |
| MethodPOST | Path/v1/connect/token |
DescriptionExchange a one-time code for a permanent API key and poll a device sign-in |
| MethodPOST | Path/v1/connect/device/authorize |
DescriptionConfirmation code for signing in from a device without a browser |
| MethodPOST | Path/v1/connect/revoke |
DescriptionRevoke a single key from the application itself |
| MethodGET | Path/v1/connect/company/status |
DescriptionDiagnostics 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.
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:
stateis generated on the server and verified on return — without this check an attacker could slip the user someone else's code.client_secretis 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_deniedthe 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:
- The button in the frame opens a window at your server's address and passes a one-time attempt identifier,
ticket, in the URL. - The server remembers the attempt and redirects the window to
/v1/connect/authorizewith thevibe:aiscope. - The user picks a Bitrix24 account and approves access. Vibecode returns the window to your
redirect_uri. - The server exchanges the code for a key, marks the attempt as finished and closes the window.
- The frame polls the server using the
ticketand shows the result.
Code in the application frame:
<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 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.
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
awaitas 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. ticketis 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
erroranderror_description, and the frame shows the reason code as is. The codes are covered in Refusal after the consent screen.
GET /v1/connect/authorize
Redirects the user to the Alaio Vibecode consent page.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
Parameterclient_id |
Typestring | Requiredyes | DescriptionPartner identifier, received when you registered the client |
Parameterredirect_uri |
Typestring | Requiredyes | DescriptionCallback URL. Must match one of those registered for the client |
Parameterstate |
Typestring | Requiredyes | DescriptionAn arbitrary string that will be returned along with the code. CSRF protection |
Parameterscope |
Typestring | Requiredno | DescriptionRequested 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 |
Parametercode_challenge |
Typestring | Requiredconditional | DescriptionSHA-256 hash of code_verifier in base64url without =. Required for public clients or when PKCE is mandatory, otherwise optional |
Parametercode_challenge_method |
Typestring | Requiredconditional | DescriptionS256, required together with code_challenge. Other methods, empty values and a method without a challenge are rejected |
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. You can also build the link for your application without code: use the "Build a link" helper on the application card, see 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
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 |
|---|---|---|
| HTTP400 | errorinvalid_request |
ConditionOne of client_id, redirect_uri, state was not passed |
| HTTP400 | errorinvalid_client |
Conditionclient_id is unknown or the client is disabled |
| HTTP400 | errorinvalid_request |
Conditionredirect_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 |
| HTTP400 | errorinvalid_request |
ConditionThe 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 |
| HTTP400 | errorinvalid_request |
Conditionredirect_uri matches none of the registered ones. error_description is Redirect URI not registered |
{
"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 |
|---|---|
error in the redirectinvalid_scope |
ConditionA right outside the client's registered set was requested |
error in the redirectinvalid_request |
ConditionRequired PKCE is missing, or the supplied code_challenge or code_challenge_method is invalid. Only S256 is supported |
error in the redirectaccess_denied |
ConditionThe user pressed "Deny" on the consent page |
error in the redirectaccess_denied |
ConditionThe 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 |
error in the redirecttemporarily_unavailable |
ConditionThe 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 exact value arrives in the x-ratelimit-limit header (the cap is divided across replicas). 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 |
|---|---|
Code in error_descriptionINT_TARIFF_REQUIRED |
What happenedThe account's Bitrix24 plan does not grant access to Vibecode. Where access requires a Vibe+ plan, INT_VIBE_PLUS_REQUIRED arrives instead |
Code in error_descriptionKEY_LIMIT_REACHED |
What happenedA key cannot be issued: the user's key limit or the platform's per-account key cap has been reached — see Key limit |
Code in error_descriptionBITRIX_UNAVAILABLE |
What happenedBitrix24 did not respond while the key was being issued |
Code in error_descriptionSTATE_EXPIRED |
What happenedThe consent link expired before confirmation — the authorization starts over from /v1/connect/authorize |
Code in error_descriptionadmin_approval_requested |
What happenedThe 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.
The full reference of general API errors — 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 |
|---|---|---|---|
Fieldgrant_type |
Typestring | Requiredyes | Descriptionauthorization_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) |
Fieldclient_id |
Typestring | Requiredyes | DescriptionPartner identifier |
Fieldclient_secret |
Typestring | Requiredyes | DescriptionPartner secret. Pass only from the server side |
Fieldcode |
Typestring | Requiredyes | DescriptionThe code from the code parameter of the redirect |
Fieldredirect_uri |
Typestring | Requiredyes | DescriptionThe same redirect_uri that was passed to /v1/connect/authorize |
Fieldcode_verifier |
Typestring | Requiredconditional | DescriptionOriginal PKCE string. Required if code_challenge was sent during authorization, including for confidential clients |
Examples
curl
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
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 |
|---|---|---|
Fieldapi_key |
Typestring | DescriptionVibecode API key in the vibe_api_... format. Pass it in the X-Api-Key header on subsequent calls |
Fieldportal.domain |
Typestring | DescriptionBitrix24 account domain, for example example.bitrix24.com |
Fieldportal.name |
Typestring | DescriptionBitrix24 account name |
Fieldscopes |
Typestring[] | DescriptionThe rights the application requested. On the consent page the user confirms the set as a whole |
Fieldgranted_scopes |
Typestring[] | DescriptionThe 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 |
Fielduser.name |
Typestring | DescriptionName of the user who granted access |
Fielduser.email |
Typestring | DescriptionThe user's email |
The two sets diverge only for applications whose scope set is assigned by the platform itself — the 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
{
"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:
{
"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 |
|---|---|---|
| HTTP400 | errorinvalid_request |
ConditionOne of client_id, code, redirect_uri was not passed |
| HTTP400 | errorinvalid_grant |
ConditionThe code does not exist, has expired, has already been used, or redirect_uri differs from the one passed to /v1/connect/authorize |
| HTTP400 | errorinvalid_grant |
ConditionThe PKCE check failed: code_verifier is missing or does not match the code_challenge sent earlier |
| HTTP400 | errorinvalid_client |
ConditionA public client sent a client_secret — a public client is issued no secret and must not send one |
| HTTP400 | errorunsupported_grant_type |
Conditiongrant_type was not passed or is not one of the modes listed in the grant_type field above |
| HTTP401 | errorinvalid_client |
Conditionclient_id is unknown, the client is disabled, or client_secret does not match |
| HTTP403 | erroraccess_denied |
ConditionThe client is in COMPANY mode, and Company key issuance is closed. code and error_description are CONNECT_COMPANY_NOT_READY; see Connection mode |
| HTTP400 | errorunauthorized_client |
ConditionA device sign-in poll from a client in COMPANY mode. code and error_description are CONNECT_COMPANY_DEVICE_FLOW_FORBIDDEN |
| HTTP429 | errorslow_down |
ConditionThe 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.
The full reference of general API errors — 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).
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 |
|---|---|
Parametercode_challenge |
ValueThe hash from step 1 |
Parametercode_challenge_method |
ValueAlways 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.
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:
{
"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 |
|---|---|
| Client typePublic | How a localhost address is matchedThe 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 |
| Client typeConfidential | How a localhost address is matchedThe 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.
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"}'
{
"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 |
|---|---|---|---|
Fieldclient_id |
Typestring | Requiredyes | DescriptionIdentifier of an application for which a platform administrator has enabled device sign-in |
Fieldscope |
Typestring | Requiredno | DescriptionRequested scopes, separated by spaces or commas. If omitted, the scopes registered for the client apply |
Fieldcode_challenge |
Typestring | Requiredconditional | DescriptionSHA-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 |
Fieldcode_challenge_method |
Typestring | Requiredconditional | DescriptionS256, required together with code_challenge |
Fielddevice_id |
Typestring | Requiredno | DescriptionA 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 |
Fieldlogin_source |
Typestring | Requiredno | DescriptionYour 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 |
|---|---|---|
| HTTP400 | errorinvalid_request |
Conditionclient_id was not passed. error_description is Missing client_id |
| HTTP401 | errorinvalid_client |
Conditionclient_id is unknown or the client is disabled |
| HTTP400 | errorunauthorized_client |
ConditionDevice sign-in is not enabled for this client. error_description is Client is not enabled for the device flow |
| HTTP400 | errorunauthorized_client |
ConditionThe client is in COMPANY mode. code and error_description are CONNECT_COMPANY_DEVICE_FLOW_FORBIDDEN |
| HTTP400 | errorinvalid_request |
ConditionThe client requires PKCE and code_challenge was not passed, or the method is not S256 or the hash is malformed |
| HTTP400 | errorinvalid_scope |
ConditionAn 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.
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 |
|---|---|
errorauthorization_pending |
What to doWait and keep polling at the interval cadence |
errorslow_down |
What to doPolling is faster than allowed. Add 5 seconds to the interval and continue |
erroraccess_denied |
What to doThe user declined. Stop polling |
erroraccess_denied with error_description=admin_approval_requested |
What to doThe 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 |
errorexpired_token |
What to doThe 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:
{ "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
intervaland react toslow_down. Polling faster than allowed does not speed up delivery — it produces refusals. - Read
Retry-Afterand 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 asRetry-Aftersays, and still increase your own pause on every refusal. - Never display
device_code. Onlyuser_codegoes 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:
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). 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 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.
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 returnsinvalid_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 three ways:
- 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.
- The application revokes its own key —
POST /v1/connect/revoke, see below. Exactly one key is revoked: the one you presented. - A platform administrator revokes the keys the client has issued.
Deactivating a client does NOT revoke keys — it only stops new issuance: with an inactive client
/v1/connect/authorizerejects 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_idortokenis missing.400 invalid_client— a public client presented aclient_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 portal. Set the server's owner and intended portal, rebind it to a key owned by that user in that portal, then retry revocation.409 KEY_OPERATION_IN_PROGRESS— another operation is changing this key. Retry when it finishes. Both refusal bodies containerror,error_descriptionandcode.429— rate limited, and there are two limiters here. The endpoint's own limiter: 60 requests per minute. The exact value arrives in thex-ratelimit-limitheader (the cap is divided across replicas). Counted by caller address alone —client_idis 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 setX-RateLimit-*headers. The platform-edge limiter: 60 requests per second per address (counted at the edge, so not divided across replicas), a bucket shared by the whole of/v1/rather than by this endpoint; it sets noX-RateLimit-*headers and itsRetry-Afteris 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 ofX-RateLimit-Limitcan. The call is not terminal: the key was not revoked, so repeat it after the pause named inRetry-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" 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_uribinding. The value in/v1/connect/authorizeand/v1/connect/tokenmust match character for character — a mismatch results ininvalid_grant.
Partner registration
You register the client yourself in the Vibecode dashboard — the 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_urivalues —https://addresses or the local127.0.0.1,localhostand[::1]overhttporhttps, - 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 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:
- Submit a request with the "Request verification" action on the application card.
- 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.
- 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 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_urireturn 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.