# Application user authorization

OAuth authorization flow for applications that act on behalf of different users of a Bitrix24 account. The user signs in through Bitrix24, the application receives a session token, and then calls the API on their behalf. An AI agent that is given the authorization key completes this whole flow on its own — following the steps below.

**Scope:** not required | **Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** the authorization key in the `app_key` parameter

## When user authorization is needed

The `vibe_app_` authorization key acts on behalf of the specific user who installed the application. To tell the platform on whose behalf to call Bitrix24, the request carries a session token in the `Authorization: Bearer` header. The token is issued at the end of this flow.

The personal `vibe_api_` key acts on behalf of its own owner and does not use a session token. For background scenarios without a user at the screen, use it instead of the authorization key — [Passing the key](/docs/keys-auth#passing-the-key).

The `/v1/oauth/*` endpoints do not read the `X-Api-Key` header. They take the authorization key as the `app_key` parameter: in the query string of `GET /v1/oauth/authorize` and `GET /v1/oauth/poll`, in the body of `POST /v1/oauth/token` and `POST /v1/oauth/placement-session`. The exception is `POST /v1/oauth/revoke`: it needs no authorization key and accepts only the session token in the `Authorization: Bearer` header.

## Step 1. Send the user to authorization

Generate a `state` — a cryptographically random string of 16–512 characters. This protects against cross-site request forgery per RFC 6749 §10.12: the client generates the value, and on return checks that the same value came back. The server cannot generate `state` for you.

Call `GET /v1/oauth/authorize` from your application server and turn off automatic redirect following. The platform checks `app_key` and `redirect_uri` and responds with `302`: the `Location` header carries the address of the Bitrix24 sign-in page. Redirect the user's browser to that address. The address in `Location` contains no authorization key, so the key stays on the server. Do not pass the `GET /v1/oauth/authorize` link itself to the browser: it contains `app_key`.

```javascript
// Runs on the application server
const state = crypto.randomUUID()
// Save state in the user's session to check it on return
const url = new URL('https://vibecode.bitrix24.com/v1/oauth/authorize')
url.searchParams.set('app_key', 'YOUR_APP_KEY')
url.searchParams.set('state', state)
url.searchParams.set('redirect_uri', 'https://myapp.example.com/callback')
const res = await fetch(url, { redirect: 'manual' })
if (res.status !== 302) throw new Error(await res.text())
const location = res.headers.get('location')
// Respond to the user's browser with a 302 redirect to location
```

If the check fails, a JSON error arrives instead of `302` — the codes are in [Errors](#errors). Open the `Location` address as a top-level navigation, not a background request: the Bitrix24 sign-in page sends an `X-Frame-Options` header and does not open inside an `iframe`. After authorization, Bitrix24 returns the user to the application's registered handler, and the platform then returns them to your `redirect_uri`.

## Step 2. Handle the return to `redirect_uri`

After authorization, the platform redirects the browser to your `redirect_uri` with a one-time code and your `state`:

```
https://myapp.example.com/callback?code=VIBE_AUTH_CODE&state=YOUR_STATE
```

Compare `state` with the value you saved in step 1. If the values do not match, abort authorization. For the errors this address can carry, see [What arrives at redirect_uri](#what-arrives-at-redirect_uri).

## Step 3. Exchange the code for a session token

From the application server, exchange `code` for a session token. The `redirect_uri` in the body must **exactly** match the one from step 1.

### cURL

```bash
curl -X POST https://vibecode.bitrix24.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "app_key": "YOUR_APP_KEY",
    "code": "VIBE_AUTH_CODE",
    "redirect_uri": "https://myapp.example.com/callback"
  }'
```

### JavaScript

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    app_key: 'YOUR_APP_KEY',
    code: 'VIBE_AUTH_CODE',
    redirect_uri: 'https://myapp.example.com/callback',
  }),
})
const { access_token, user, expires_in } = await res.json()
// Save access_token — it is valid for 24 hours
```

```json
{
  "success": true,
  "access_token": "vibe_session_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "user": { "id": "42", "name": "John Smith", "email": "user@example.com" },
  "expires_in": 86400
}
```

The code is single-use and valid for 5 minutes.

## Step 4. Call the API with the session token

Every API request carries two headers — the authorization key in `X-Api-Key` and the session token in `Authorization: Bearer`:

### cURL

```bash
curl https://vibecode.bitrix24.com/v1/deals \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/deals', {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})
const { data } = await res.json()
```

The session inherits the application's rights in the Bitrix24 account. Without the `Authorization: Bearer` header, endpoints that require a user context return `401 TOKEN_MISSING`.

## What arrives at `redirect_uri`

The platform returns the user to `redirect_uri` with the result in query parameters. Your `state` always comes back — check it first.

| Parameter | When it arrives | Value |
|----------|----------------|----------|
| `code` | Authorization succeeded | One-time code to [exchange for a token](#step-3-exchange-the-code-for-a-session-token). Valid for 5 minutes |
| `state` | Always | The `state` value passed in step 1 |
| `error` | Authorization failed | Reason for the denial — one of the values below |

`error` values:

| `error` | What happened |
|---------|---------------|
| `token_exchange_failed` | Bitrix24 did not issue tokens for the authorization code |
| `invalid_domain` | The Bitrix24 account domain failed validation |
| `profile_fetch_failed` | Failed to read the user profile in the Bitrix24 account |
| `app_unavailable` | The application is no longer available: its author erased their data |

Some failures at the return step arrive as a JSON response with status `400` or `500` instead of in the address: until the platform knows `redirect_uri`, it cannot redirect to it. These are `MISSING_PARAMS`, `INVALID_STATE`, and `APP_CONFIG_ERROR` from the [Errors](#errors) table.

## `redirect_uri` rule

`redirect_uri` must **exactly** match one of the addresses registered for the application — scheme, host, port, and path all match. You can add a return address in the application card in the [Auth Keys](/apps) section — the "Redirect URI (OAuth)" field.

A new application has the address `http://localhost` registered by default — without a port and with the path `/`. The match is exact, so `http://localhost:3000/callback` does not match it. For the return to go to your address — `http://localhost:3000/callback` or `https://myapp.example.com/callback` — first add it in the application card. Otherwise `GET /v1/oauth/authorize` returns `400 INVALID_REDIRECT_URI`.

If `redirect_uri` is omitted, the platform uses the built-in return page — [Authorization without your own redirect_uri](#authorization-without-your-own-redirect_uri).

## Authorization without your own `redirect_uri`

When `redirect_uri` is omitted, the platform returns the user to the built-in `/oauth/complete` page and creates the session token on its side. The application server retrieves the result by polling `GET /v1/oauth/poll` with the same `state`. Do not make this request from the browser: its URL contains `app_key`.

### cURL

```bash
curl "https://vibecode.bitrix24.com/v1/oauth/poll?app_key=YOUR_APP_KEY&state=YOUR_STATE"
```

### JavaScript

```javascript
// Runs on the application server
const url = `https://vibecode.bitrix24.com/v1/oauth/poll?app_key=YOUR_APP_KEY&state=${state}`
const res = await fetch(url)
const result = await res.json()
if (result.status === 'complete') {
  // result.access_token, result.user, result.expires_in
}
```

Until the user completes sign-in, the response carries status `pending`:

```json
{ "success": true, "status": "pending" }
```

After sign-in, the same request returns the session token. The result is single-use — the next poll with this same `state` returns `pending` again:

```json
{
  "success": true,
  "status": "complete",
  "access_token": "vibe_session_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "user": { "id": "42", "name": "John Smith", "email": "user@example.com" },
  "expires_in": 86400
}
```

The result is kept for 10 minutes. Retrieve it right after the user returns to the sign-in page.

## Session token: lifetime and revocation

The session token is valid for 24 hours and is not refreshed — the `expires_in` field in the response equals `86400`. There is no renewal mechanism. After expiry, calls on behalf of the user return `401 INVALID_SESSION` — this is the signal to go through authorization again: `GET /v1/oauth/authorize` → `POST /v1/oauth/token`.

To revoke a token early, call `POST /v1/oauth/revoke` with the session token in the `Authorization: Bearer` header. Revocation deletes the user's session, and further calls with this token are rejected.

### cURL

```bash
curl -X POST https://vibecode.bitrix24.com/v1/oauth/revoke \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/oauth/revoke', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer USER_SESSION_TOKEN' },
})
const { revoked } = await res.json()
```

```json
{ "success": true, "revoked": true }
```

The `revoked` field equals `false` if there was no longer an active session with that token. The response arrives with status `200` in both cases.

## Application on your own server

A separate tab for sign-in is not always available — for example, the application opens as a placement inside Bitrix24, and the consent page does not open inside an `iframe`. If the application is hosted on your own server rather than behind Black Hole, the current user's session token is obtained in one of two ways — depending on whose handler receives the placement.

**Vibecode application whose `appUrl` points to your server.** The placement arrives at the Vibecode handler, and the platform redirects the browser to `<appUrl>/?code=<one-time code>&placement=...&member_id=...`. Your server exchanges the code for a session token using the same [`POST /v1/oauth/token`](#step-3-exchange-the-code-for-a-session-token) request with the body `{ app_key, code, redirect_uri }`, where `redirect_uri` exactly equals the configured `appUrl`. The session token does not reach the browser — the address holds only a one-time code, useless without `app_key`. Edit the `appUrl` field in the application card in the [Auth Keys](/apps) section.

**Your own Bitrix24 application with its own handler.** The placement arrives directly at your server with a Bitrix24 user token. Exchange it for a session token with a server request — `POST /v1/oauth/placement-session`.

### cURL

```bash
curl -X POST https://vibecode.bitrix24.com/v1/oauth/placement-session \
  -H "Content-Type: application/json" \
  -d '{
    "app_key": "YOUR_APP_KEY",
    "access_token": "BITRIX24_USER_TOKEN",
    "member_id": "MEMBER_ID",
    "domain": "mycompany.bitrix24.com"
  }'
```

### JavaScript

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/oauth/placement-session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    app_key: 'YOUR_APP_KEY',
    access_token: 'BITRIX24_USER_TOKEN',
    member_id: 'MEMBER_ID',
    domain: 'mycompany.bitrix24.com',
  }),
})
const { access_token, user, expires_in } = await res.json()
```

```json
{
  "success": true,
  "access_token": "vibe_session_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "user": { "id": "42", "name": "John Smith", "email": "user@example.com" },
  "expires_in": 86400
}
```

`domain` must match the application's Bitrix24 account. The `refresh_token` and `expires_in` fields in the body are optional and describe the Bitrix24 user token: `expires_in` — how long it is valid, 3600 seconds by default. Without an active `refresh_token`, the Bitrix24 token is not refreshed automatically — after it expires, reopen the placement. The session token itself, as in the main flow, is valid for 24 hours.

For the full lifecycle of an embedded application and handler examples in Node, Python, and Go, see [Authorization in a Black Hole app](/docs/infra/app-runtime).

## Endpoint reference

| Method | Path | Description |
|-------|------|----------|
| GET | [`/v1/oauth/authorize`](#step-1-send-the-user-to-authorization) | Start authorization — redirect to the Bitrix24 sign-in page |
| GET | [`/v1/oauth/callback`](#what-arrives-at-redirect_uri) | Receive the Bitrix24 response. Called by Bitrix24, not the application |
| POST | [`/v1/oauth/token`](#step-3-exchange-the-code-for-a-session-token) | Exchange the code for a session token |
| GET | [`/v1/oauth/poll`](#authorization-without-your-own-redirect_uri) | Retrieve the authorization result without your own `redirect_uri` |
| POST | [`/v1/oauth/placement-session`](#application-on-your-own-server) | Session token for an application on your own server |
| POST | [`/v1/oauth/revoke`](#session-token-lifetime-and-revocation) | Revoke the session token |

## Errors

The authorization key is checked on entry to `GET /v1/oauth/authorize`, `POST /v1/oauth/token`, and `POST /v1/oauth/placement-session`:

| HTTP | Code | Description |
|------|-----|----------|
| 401 | `INVALID_APP_KEY` | `app_key` not found or not an authorization key |
| 401 | `KEY_INACTIVE` | Authorization key revoked |
| 401 | `KEY_EXPIRED` | Authorization key has expired |
| 403 | `OWNER_BLOCKED` | The key owner is blocked or no longer active on the portal |

`GET /v1/oauth/authorize`:

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | The required `state` is missing or outside the 16–512 character range |
| 400 | `INVALID_REQUEST` | `state` starts with `open-app:` — the platform reserves this prefix |
| 400 | `INVALID_REDIRECT_URI` | `redirect_uri` is not registered for the application |
| 500 | `NO_PORTAL` | The application is not linked to a portal |

`GET /v1/oauth/callback` (called by Bitrix24):

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `MISSING_PARAMS` | Bitrix24 returned without `code` or `state` |
| 400 | `INVALID_STATE` | `state` not found or expired after 20 minutes. `error.details.reason` holds `NOT_FOUND` or `EXPIRED` |
| 500 | `APP_CONFIG_ERROR` | The application has no OAuth credentials set |

`POST /v1/oauth/token`:

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | The body has no `code` or `redirect_uri` |
| 400 | `INVALID_CODE` | Code not found or belongs to another application |
| 400 | `CODE_EXPIRED` | The code has expired — it is valid for 5 minutes |
| 400 | `CODE_ALREADY_USED` | The code has already been exchanged for a token |
| 400 | `REDIRECT_URI_MISMATCH` | `redirect_uri` does not match the one passed to `GET /v1/oauth/authorize` |
| 500 | `TOKEN_NOT_FOUND` | User token not found |

`GET /v1/oauth/poll`:

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | `state` shorter than 16 characters or no `app_key` |
| 401 | `INVALID_APP_KEY` | `app_key` not found or not an authorization key |

`POST /v1/oauth/placement-session`:

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | The body has no `access_token`, `member_id`, or `domain` |
| 400 | `DOMAIN_MISMATCH` | `domain` does not match the application's portal |
| 401 | `USER_AUTH_REQUIRED` | The Bitrix24 user token did not confirm authorization on the portal |
| 403 | `APP_UNAVAILABLE` | The application is no longer available: its author erased their data |
| 500 | `NO_PORTAL` | The application is not linked to a portal |

`POST /v1/oauth/revoke`:

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `MISSING_TOKEN` | No `Authorization: Bearer` header with a session token was passed |

API calls with a session token:

| HTTP | Code | Description |
|------|------|-------------|
| 401 | `TOKEN_MISSING` | The authorization key was sent without `Authorization: Bearer` to an endpoint that needs a user context |
| 401 | `INVALID_SESSION` | The session token has expired, been revoked or is unknown. Authorize again: `GET /v1/oauth/authorize` → `POST /v1/oauth/token` |

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

## Error response example

400 — required `state` not passed:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "state: Required"
  }
}
```

## See also

- [Keys and authorization](/docs/keys-auth)
- [Key self-description](/docs/keys-auth/me)
- [Authorization in a Black Hole app](/docs/infra/app-runtime)
- [Embedding an app in the Bitrix24 account](/docs/keys-auth#embedding-an-app-in-the-bitrix24-account)
- [Errors](/docs/errors)
