# Application user authorization

OAuth authorization flow for applications that act on behalf of different account users. 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:** `X-Api-Key: YOUR_APP_KEY`

## When user authorization is needed

The `vibe_app_` authorization key acts on behalf of the specific user who installed the application. So that the platform knows 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).

All `/v1/oauth/*` endpoints respond to a single `X-Api-Key` header holding the authorization key, without `Authorization: Bearer`.

## 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.

Build a link to `GET /v1/oauth/authorize` and redirect the user's browser to it. This is a top-level navigation, not a background request — the Bitrix24 consent page sends an `X-Frame-Options` header and does not open inside an `iframe`.

```javascript
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')
window.location.href = url.toString()
```

The platform checks `app_key` and `redirect_uri` and redirects the user to the Bitrix24 authorization page. After authorization, Bitrix24 returns the user to the application's registered handler, and the platform 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. Parsing the errors in this address — [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 is **exactly** equal to 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 one-time 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 on the 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 portal domain failed validation |
| `profile_fetch_failed` | Failed to read the user profile on the portal |
| `app_unavailable` | The application is no longer available: its author erased their data |

Some failures at the return step arrive not in the address but as a JSON response with status `400` or `500` — when the platform does not yet know `redirect_uri`, it cannot return 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 [Authorization 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 retrieves the result by polling `GET /v1/oauth/poll` with the same `state`.

### cURL

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

### JavaScript

```javascript
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, status `pending` arrives:

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

After sign-in, the same request returns the session token. The result is one-time — 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 signals to go through authorization again: `GET /v1/oauth/authorize` → `POST /v1/oauth/token`.

To revoke a token early — `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 such a 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 BlackHole, the current user's session token is obtained in one of two ways — depending on whose handler receives the placement.

**Vibecode application, `appUrl` — 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 with the same [`POST /v1/oauth/token`](#step-3-exchange-the-code-for-a-session-token) request and 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`. The `appUrl` field is edited in the application card in the [Authorization 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 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.

The full lifecycle of an embedded application and handler examples in Node, Python, and Go — [Authorization in a BlackHole application](/docs/infra/app-runtime).

## Endpoint reference

| Method | Path | Description |
|-------|------|----------|
| GET | `/v1/oauth/authorize` | Start authorization — redirect to the Bitrix24 sign-in page |
| GET | `/v1/oauth/callback` | Receive the Bitrix24 response. Called by Bitrix24, not the application |
| POST | `/v1/oauth/token` | Exchange the code for a session token |
| GET | `/v1/oauth/poll` | Retrieve the authorization result without your own `redirect_uri` |
| POST | `/v1/oauth/placement-session` | Session token for an application on your own server |
| POST | `/v1/oauth/revoke` | 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 it is 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 by the platform |

`GET /v1/oauth/authorize`:

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | Missing required `state` or it is outside the 16–512 character range |
| 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 — 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 it is 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` with a session token was passed |

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

- [Creating and using a key](/docs/keys-auth)
- [Key self-description](/docs/keys-auth/me)
- [Authorization in a BlackHole application](/docs/infra/app-runtime)
- [Embedding an app in the account](/docs/keys-auth#embedding-an-app-in-the-bitrix24-account)
- [Errors](/docs/errors)
