For AI agents: markdown of this page — /docs-content-en/cowork/app-login.md documentation index — /llms.txt

Sign in to an app from the desktop

POST /v1/cowork/app-login

Gives the Cowork/Code desktop app a sign-in link to a deployed app, so the built-in browser opens the app already signed in as the key owner, without a sign-in form.

The platform decides access itself: the app must belong to the Bitrix24 account the key is bound to, and the app's access policy must admit the key owner. The gateway exchanges the token in the address for its own cookie on the app subdomain and redirects to the address without the token. The app receives the user's Bitrix24 ID, as with a regular sign-in. The token opens only this app: it opens neither the platform dashboard nor other apps.

A sign-in link is issued for an app on a dedicated server and for a Galaxy app: both go through the same access check. The shared galaxy host is not an app, and a request with its address gets 404 APP_NOT_AVAILABLE.

Only a Cowork/Code desktop key with the vibe:cowork scope can call this endpoint. Agent seat keys and third-party agent keys carry the same scope but receive 403 COWORK_DESKTOP_KEY_REQUIRED.

Scope: vibe:cowork. The key must also be a Cowork/Code desktop key.

Request fields (body)

Field Type Required Description
appUrl string yes Address of the app to open, with any path, query parameters and fragment. The scheme, host and port must match the app address exactly. User info in the address and the __gw_token parameter are not allowed

Examples

curl

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/cowork/app-login \
  -H "X-Api-Key: YOUR_COWORK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"appUrl": "https://app-3f9c2a1b7e.vibecode.bitrix24.com/orders?status=new"}'

JavaScript — get the sign-in address and check it

javascript
// An app address: https, an app-* subdomain of the app domain, no port, user info, or gateway token
function isAppAddress(appUrl) {
  try {
    const u = new URL(appUrl)
    return u.protocol === 'https:' && !u.port && !u.username && !u.password
      && !u.searchParams.has('__gw_token')
      && /^app-[a-z0-9-]+\.vibecode\.bitrix24\.com$/.test(u.hostname)
  } catch {
    return false
  }
}

async function getAppLoginUrl(appUrl) {
  // Not an app address: do not call the endpoint and do not open the address
  if (!isAppAddress(appUrl)) return null
  const res = await fetch('https://vibecode.bitrix24.com/v1/cowork/app-login', {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_COWORK_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ appUrl }),
  })
  if (!res.ok) {
    const { error } = await res.json().catch(() => ({}))
    // The platform did not recognize the address as an app: do not open it at all
    if (error?.code === 'INVALID_APP_URL') return null
    return appUrl // any other refusal: the address was checked above, open it with the regular sign-in
  }

  const { url } = await res.json() // { url, expiresIn } — no success/data wrapper
  // The response differs from the request by exactly one token parameter
  const stripped = url.replace(/[?&]__gw_token=[^&#]*/, '')
  return stripped === appUrl ? url : null // a difference remains: open nothing
}

Response fields

Field Type Description
url string The original appUrl with one added __gw_token parameter. The parameter is the last one in the query string, before the fragment. Do not parse the token and do not write it to logs
expiresIn number Token lifetime in seconds. The platform takes it from the signed token itself

Response example

JSON
{
  "url": "https://app-3f9c2a1b7e.vibecode.bitrix24.com/orders?status=new&__gw_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJndyJ9.c2lnbmF0dXJl",
  "expiresIn": 600
}

Error response example

404 — the app is not available:

JSON
{
  "success": false,
  "error": {
    "code": "APP_NOT_AVAILABLE",
    "message": "The app is not available."
  }
}

Errors

HTTP Code Description
400 INVALID_APP_URL The appUrl field is missing, is not a string, or is not the exact address of an app: a different scheme or port, an extra subdomain level, user info, control characters, or an existing __gw_token parameter. Retrying with the same body will not help
401 MISSING_API_KEY The X-Api-Key header is missing
401 INVALID_API_KEY The key is not recognized — no such key exists on the platform
401 KEY_INACTIVE The key has been revoked, for example when the device was signed out with DELETE /v1/cowork/key
401 KEY_EXPIRED The key has expired
402 ACCOUNT_FROZEN The account balance is exhausted — top up to continue
403 INSUFFICIENT_SCOPE The key lacks the vibe:cowork scope
403 COWORK_DESKTOP_KEY_REQUIRED The key does not belong to the Cowork/Code desktop class. This response applies to both agent seat keys and third-party agent keys
403 PORTAL_REQUIRED The key is not bound to a person on a portal
403 WRITE_BLOCKED_READONLY_KEY The key was issued in read-only mode
404 APP_NOT_AVAILABLE One answer for every access outcome: the app does not exist, belongs to another account, its access policy does not admit the key owner, or the platform could not decide
409 B24_USER_UNKNOWN The key owner's user ID in Bitrix24 could not be determined, so there is no identity to vouch for
413 PAYLOAD_TOO_LARGE The body is larger than 8 KB
415 FST_ERR_CTP_INVALID_MEDIA_TYPE The request body uses a content type this route does not parse. Send the body with the Content-Type: application/json header
429 RATE_LIMITED The rate limit for the account and person combination has been exceeded: several keys of one person share the same limit. The platform-wide limit is 30 requests per minute. The effective limit for your key is returned in the x-ratelimit-limit header. It is lower than the platform-wide limit because that limit is divided across replicas
503 user_self_deletion_pending The key owner is pending account deletion on this Bitrix24 account. The request is refused before a token is issued; retry after the delay in Retry-After

Full list of common API errors: Errors.

Known specifics

A successful response (200) is the object itself, with no success wrapper. Errors come in the { success: false, error: { code, message } } envelope. Determine success by the HTTP status (res.ok).

The response address matches the request byte for byte, except for the token parameter. The platform does not rebuild the address: parameter encoding, empty values and the fragment stay as they are in appUrl. Compare the response with the request after removing the __gw_token parameter, and do not open the address if any difference remains. After exchanging the token for a cookie, the gateway redirects to the address without the token and rebuilds the query string, so the parameter order and the way empty values are written may differ from appUrl by the time the app receives it.

The token stays valid until it expires, not for a single use. Following the same address again within expiresIn signs the user in to the app again. After the first visit, the gateway sets a cookie and redirects to the address without the token, so later visits to the same app use the cookie, with no new call.

Revoking the key stops new sign-ins from being issued. A cookie the gateway has already set lives out its lifetime. The gateway rechecks the app's access policy itself, so revoking access to the app also applies to such a cookie.

How to react to refusals. After 400 INVALID_APP_URL do not open the address at all: the platform did not recognize it as an app address, so show the user an error. Any other refusal means the app should be opened at the original address with the regular sign-in, but only if the address passed your own check before the request: https, an app-* subdomain of the app domain, no port, user info, or __gw_token parameter. A 401 or 403 arrives before the address is parsed and does not confirm that it is an app address. 401: ask the user to sign in to the desktop app again. 400 and 403: a client error, so retrying is pointless. 429: retry with a growing delay.

See also