
## Mint an access token

`POST /v1/infra/servers/:id/access-tokens`

Mints a short-lived token for external access to a deployed application. Two modes: `api-bearer` — a JWT for the `Authorization` HTTP header; `share-url` — a link that sets a cookie when visited.

A development-team key with the `ADMIN` or `DEVELOPER` role can mint only `api-bearer`. On the first request, supply `ttlSeconds` between 300 and 600. When retrying an approved operation, supply the exact `expiresAt` from `retryOperation` and the approved request ID as `approvalId`, without `ttlSeconds`. See [Approving wider access](/docs/infra/access#approving-wider-access) for the approval flow.

Recovery depends on the calling key. After `410 TOKEN_EXPIRED`, the token record may already exist even though no JWT was returned. Use the key that manages the server to [list tokens](./list.md) with `status=all` before another mint. A development-team caller must ask that key holder to reconcile. Development-team keys cannot call refresh or mint `share-url`; further access requires a separately approved new `api-bearer` mint.

## Parameters

| Parameter | In | Type | Required | Description |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | yes | BLACKHOLE server ID. List: [`GET /v1/infra/servers`](/docs/infra/servers/list) |

## Request body fields

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|----------|
| `mode` | string | yes | — | `"api-bearer"` or `"share-url"` |
| `ttlSeconds` | number | no | `86400` | Token lifetime in seconds. Range: 300–315,360,000 (from 5 minutes to 10 years). The value 315,360,000 is shown in the Vibecode dashboard as "No expiration" |
| `expiresAt` | string (ISO 8601) | no | — | On an approved retry, the exact value from `retryOperation`, without `ttlSeconds` |
| `approvalId` | string (UUID) | no | — | Approved request ID from `error.details.proposalId`. Retry the original action with the same key and the approved parameters |
| `identityBound` | boolean | no | `true` | For `share-url` only. When `true`, login via Bitrix24 is required and the real user identifier is recorded in the log. When `false`, the visit is anonymous and a synthetic identifier is recorded |
| `name` | string | no | — | Token label for display in the list (up to 100 characters) |

## Access approval

If minting expands access, the first request returns `409 AUDIENCE_APPROVAL_REQUIRED`. Its `error.details` contains `proposalId`, `approvalUrl`, `expiresAt` and `retryOperation`. No token is minted at this step. Send the URL to a human authorized to approve access and signed in to their Vibecode account; an API key cannot approve its own request. Use the same key to [read the request state](/docs/infra/access/audience-proposals-get). After `APPROVED`, start with the original HTTP body, remove `ttlSeconds`, and add only `expiresAt` from `retryOperation` and `approvalId` equal to `proposalId`. The `retryOperation` object is not an HTTP body: do not send its internal `type` or uppercase mode. Do not recalculate expiry or change mode, name or identity binding.

`APPROVED` means consent, not a minted token. If parameters or access changed, or approval was consumed or expired, branch on the error code in the table below. With `410 TOKEN_EXPIRED`, a record may already exist but no JWT was returned: ask the holder of the key that manages the server to [list and reconcile token records](./list.md) with `status=all` before retrying, and obtain separate approval for a further mint. A development-team key cannot list tokens.

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/access-tokens" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "api-bearer",
    "ttlSeconds": 600,
    "name": "ci-smoke"
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/access-tokens" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "api-bearer",
    "ttlSeconds": 600,
    "name": "ci-smoke"
  }'
```

### JavaScript — personal key

```javascript
const requestBody = { mode: 'api-bearer', ttlSeconds: 600, name: 'ci-smoke' }
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/access-tokens`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(requestBody),
  }
)
const result = await res.json()
if (!res.ok || result.success !== true) {
  if (res.status === 409 && result.error?.code === 'AUDIENCE_APPROVAL_REQUIRED') {
    const { proposalId, approvalUrl, retryOperation } = result.error.details
    const { ttlSeconds, ...sameIntent } = requestBody
    const retryBody = { ...sameIntent, expiresAt: retryOperation.expiresAt, approvalId: proposalId }
    // Send approvalUrl to a human; after APPROVED, submit retryBody with the same key.
    console.log({ proposalId, approvalUrl, retryBody })
    throw new Error('Access approval is pending')
  }
  throw new Error(result.error?.code ?? `HTTP_${res.status}`)
}
const { data } = result

// E2E check via the public path
const check = await fetch(`${data.appUrl}/api/health`, {
  headers: { Authorization: `Bearer ${data.token}` },
})
console.log(check.status) // 200 — the application responds through the Gateway
```

### JavaScript — OAuth application

```javascript
const requestBody = { mode: 'api-bearer', ttlSeconds: 600, name: 'ci-smoke' }
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/access-tokens`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(requestBody),
  }
)
const result = await res.json()
if (!res.ok || result.success !== true) {
  if (res.status === 409 && result.error?.code === 'AUDIENCE_APPROVAL_REQUIRED') {
    const { proposalId, approvalUrl, retryOperation } = result.error.details
    const { ttlSeconds, ...sameIntent } = requestBody
    const retryBody = { ...sameIntent, expiresAt: retryOperation.expiresAt, approvalId: proposalId }
    // Send approvalUrl to a human; after APPROVED, submit retryBody with the same key.
    console.log({ proposalId, approvalUrl, retryBody })
    throw new Error('Access approval is pending')
  }
  throw new Error(result.error?.code ?? `HTTP_${res.status}`)
}
const { data } = result

// E2E check via the public path
const check = await fetch(`${data.appUrl}/api/health`, {
  headers: { Authorization: `Bearer ${data.token}` },
})
console.log(check.status) // 200 — the application responds through the Gateway
```

## Response fields

The set of fields depends on the mode.

**`api-bearer` mode:**

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.id` | string | Token ID for later revocation |
| `data.mode` | string | `"api-bearer"` |
| `data.token` | string | JWT for the `Authorization: Bearer` header. Save it right away: this exact string is not returned again, and a fresh JWT for a record issued by the key that manages the server comes from [token refresh](./refresh.md). A development-team key cannot refresh tokens and needs a separately approved new mint |
| `data.expiresAt` | string (ISO 8601) | Retention period of the token record — for listing and revocation |
| `data.jwtExpiresAt` | string (ISO 8601) | The actual expiry of the Bearer token. Capped at 10 minutes regardless of `ttlSeconds`. After it expires, mint a new token |
| `data.note` | string | Note about the difference between `expiresAt` and `jwtExpiresAt` |
| `data.subdomain` | string | Server subdomain |
| `data.appUrl` | string | Full HTTPS address of the application |
| `data.curlExample` | string | Ready-to-use `curl` example with the token for a quick check |

**`share-url` mode:**

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.id` | string | Token ID for later revocation |
| `data.mode` | string | `"share-url"` |
| `data.shortcode` | string | Code inserted into the URL as `?s=<shortcode>` |
| `data.url` | string | The full shareable link |
| `data.identityBound` | boolean | Whether a visit requires login via Bitrix24 |
| `data.expiresAt` | string (ISO 8601) | Token expiration time |
| `data.name` | string \| null | The label passed at mint time |

## Response example

**`api-bearer` mode:**

```json
{
  "success": true,
  "data": {
    "id": "9f1c4b7e-3d52-4a18-9c0e-7b2a1f6d84c3",
    "mode": "api-bearer",
    "token": "eyJhbGciOiJFUzI1NiJ9...",
    "expiresAt": "2026-05-18T10:50:00.000Z",
    "jwtExpiresAt": "2026-05-18T10:40:10.000Z",
    "subdomain": "app-91306a4c",
    "appUrl": "https://app-91306a4c.vibecode.bitrix24.com",
    "curlExample": "curl -H \"Authorization: Bearer eyJhbGciOiJFUzI1NiJ9...\" https://app-91306a4c.vibecode.bitrix24.com/api/health",
    "note": "JWT is a 10-minute Gateway session token. The row's `expiresAt` is the long-lived TTL for listing/revoking, but the Bearer token itself stops working at `jwtExpiresAt`. To keep a long-running client authenticated, POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh before jwtExpiresAt to re-mint a fresh JWT for the SAME token (no new row — does not consume the active-token cap or mint rate-limit), or use mode=share-url for browser links that auto-refresh on each visit."
  }
}
```

**`share-url` mode:**

```json
{
  "success": true,
  "data": {
    "id": "2a7d5e61-84bc-4f39-b0d7-5e6c9a3f1b28",
    "mode": "share-url",
    "shortcode": "R8k3Zm2P",
    "url": "https://app-91306a4c.vibecode.bitrix24.com/?s=R8k3Zm2P",
    "identityBound": false,
    "expiresAt": "2026-06-17T08:44:00.000Z",
    "name": "preview"
  }
}
```

## Error response example

429 — token mint rate limit exceeded. The count is kept in the database, so the number in the response is exact:

```json
{
  "success": false,
  "error": {
    "code": "TOKEN_MINT_RATE_LIMIT",
    "message": "Rate limit: 50 mints/hour per API key"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_MODE` | An invalid `mode` was passed, or a field in the body has the wrong value type |
| 400 | `UNKNOWN_PARAM` | The request body contains an unknown field. The response contains `details` with the list of valid fields and a suggestion |
| 400 | `INVALID_TTL` | `ttlSeconds` is outside the allowed range [300, 315,360,000]. A development-team key must supply `ttlSeconds` between 300 and 600 on the first request, or the approved `expiresAt` with no more than 600 seconds remaining on retry |
| 400 | `NAME_TOO_LONG` | `name` exceeds 100 characters |
| 400 | `SERVER_NO_SUBDOMAIN` | The server has no subdomain, so there is nothing to send requests to |
| 401 | `MISSING_API_KEY` | No API key was supplied through `X-Api-Key` or a supported `Authorization: Bearer` scheme |
| 401 | `INVALID_API_KEY` | The authentication key is not recognized |
| 401 | `KEY_INACTIVE` | The requesting API key is no longer active |
| 401 | `KEY_EXPIRED` | The requesting API key has expired; this is distinct from resource URL expiry |
| 401 | `NO_PORTAL` | A non-management key has no portal binding |
| 401 | `COWORK_CHAT_KEY_INACTIVE` | The parent device key behind a Cowork chat key is no longer usable |
| 401 | `WRONG_AUTH_SCHEME` | An OAuth application key was sent as a Bearer session: put the key in `X-Api-Key` and the session in `Authorization: Bearer` |
| 401 | `INVALID_SESSION` | The supplied OAuth session is invalid or expired |
| 401 | `TOKEN_EXPIRED` | The OAuth user token expired and its refresh failed; re-authorization is required |
| 401 | `TOKEN_INVALID` | When the partner AI plane is enabled, its supplied token is unknown or expired |
| 401 | `KEY_REVOKED` | The partner credential behind the AI plane token is revoked |
| 402 | `ACCOUNT_FROZEN` | The key owner’s account is frozen. With wallet-debt scoping disabled, the shared API-key guard refuses this call before its handler. Follow error.topupUrl to resolve the balance; this status alone does not authorize repeating a prior write. |
| 403 | `AUDIENCE_APPROVAL_FORBIDDEN` | The current mint source failed the authority check: for example, the key, its owner or the portal is no longer active, the owner's membership ended, or the approving human lost authority. No token was minted. Restore current authority or use an appropriate key; for an approval, obtain new consent from a human with current authority |
| 403 | `FEATURE_DISABLED` | Mint passed the preliminary check before the feature was disabled; the fresh transactional check refused it. No token was minted |
| 403 | `TOKEN_OWNER_MISMATCH` | The server belongs to a different API key. An exception is a development-team key with a current `ADMIN` or `DEVELOPER` role on this server. |
| 403 | `SERVER_KEY_MISMATCH` | While the token was being issued, the server moved to another API key — for example, during a key rotation — or was deleted. No token was issued. Retrieve the server again with [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) and issue the token with the key that currently owns the server. If the server was deleted, that request returns `404` |
| 403 | `PORTAL_COLLABORATOR_TOKEN_MODE_FORBIDDEN` | A development-team key requested `share-url`. This key can mint only `api-bearer` |
| 403 | `AGENT_OWNER_ONLY` | The server was created for an AI agent — access tokens are disabled for it |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | The call was made with a Cowork key — such a key works with data only and cannot perform write operations. To issue a key that can, see [Project key for deploy](/docs/cowork/deploy-key) |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | A read-only key cannot perform this write. |
| 403 | `EXTERNAL_COLLABORATOR_KEY_OUT_OF_SCOPE` | This operation is outside the external-collaborator key route list. Use the key that manages the server |
| 403 | `PORTAL_COLLABORATOR_KEY_WRONG_SERVER` | The development-team key is bound to another server |
| 403 | `PORTAL_COLLABORATOR_NOT_A_MEMBER` | Development-team membership ended or the current role no longer permits this operation |
| 403 | `IP_NOT_ALLOWED` | The caller IP is outside the key allowlist. |
| 403 | `OWNER_BLOCKED` | The key owner is blocked. |
| 403 | `OWNER_DELETED` | The key owner was deleted. |
| 403 | `PORTAL_SUSPENDED` | The bound portal is suspended. |
| 403 | `PORTAL_DELETED` | The bound portal was deleted. |
| 403 | `PORTAL_BLOCKED` | The bound portal is blocked. |
| 403 | `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | The management key lacks the portal binding or entity authority required by this call. |
| 403 | `AGENT_MAINTENANCE_KEY_OUT_OF_SCOPE` | The maintenance key route list does not include this operation. |
| 403 | `APP_API_KEY_OUT_OF_SCOPE` | The application key route list does not include this operation. |
| 403 | `WAITLIST_PENDING` | The key owner has not been activated. |
| 403 | `SESSION_APP_MISMATCH` | The OAuth session belongs to another application. |
| 403 | `PLANE_FORBIDDEN` | The partner key does not allow this AI plane. |
| 403 | `SYSTEM_FORBIDDEN` | The partner key does not allow this AI system. |
| 403 | `SCOPE_FORBIDDEN` | The partner AI plane forbids this route. |
| 403 | `CONNECT_COMPANY_DERIVED_TOKEN_FORBIDDEN` | A company connection key cannot manage derived tokens. |
| 403 | `COWORK_READONLY` | The Cowork seat is read-only. |
| 403 | `GALAXY_DISABLED` | Galaxy operations are disabled for this portal. |
| 404 | `NOT_FOUND` | The server does not exist, was deleted, or exists and belongs to the calling key but its portal has been deactivated; or `approvalId` was not found or is inaccessible to the calling key |
| 409 | `ACTIVE_TOKEN_LIMIT` | The limit of 100 active tokens per server has been reached |
| 409 | `KEY_ROTATION_RECOVERY_REQUIRED` | The replacement of the API key that made the request is unfinished: its rotation ended with a `500` response or has not finished yet. No token was issued. The steps are in [Rotate an API key](/docs/management-keys/rotate) |
| 409 | `AUDIENCE_APPROVAL_REQUIRED` | Human consent is required. Use `error.details.proposalId`, `approvalUrl` and `retryOperation` following the approval flow above |
| 409 | `AUDIENCE_APPROVAL_MISMATCH` | The key, parameters or expected result differs from the approved action. Keep the approved retry unchanged |
| 409 | `AUDIENCE_APPROVAL_STALE` | Access or its revision changed after the request was created. Obtain new approval |
| 409 | `AUDIENCE_APPROVAL_REPLAY` | Approval was consumed or is no longer available to apply. Read the request state again |
| 410 | `AUDIENCE_APPROVAL_EXPIRED` | Approval expired. Create a new request and obtain consent again |
| 410 | `TOKEN_EXPIRED` | The persisted record expired before its JWT could be signed. No JWT was returned; have the holder of the key that manages the server reconcile token records with `status=all` before another mint; a development-team key cannot list tokens |
| 429 | `QUOTA_EXCEEDED` | API billing is enabled with a nonzero overage price: the daily free quota is exhausted and the PREPAY balance is not positive. Top up the balance. `error.quota` and `error.used` report counts; `X-RateLimit-Quota`, `X-RateLimit-Used` and `X-RateLimit-Remaining` report the quota, usage and zero remainder. This refusal does not promise `Retry-After` or `X-RateLimit-Scope` |
| 429 | `CALLER_RATE_LIMITED` | The shared per-key or per-user request limit was exceeded. Wait for the number of seconds specified in `Retry-After`; `X-RateLimit-Scope` identifies the refusing layer |
| 429 | `TOKEN_MINT_RATE_LIMIT` | The limit of 50 mints per hour per API key has been exceeded. The count is kept in the database and is not divided across replicas. Header `Retry-After: 3600` |
| 503 | `FEATURE_DISABLED` | The access-tokens section is disabled on the platform. The pre-call signal and the fallback are described in [Availability](/docs/infra/access-tokens#availability) |
| 503 | `ACCOUNT_PENDING_ERASURE` | The key owner is pending erasure or being erased. Header `Retry-After: 3600`. |
| 503 | `SERVICE_DISABLED` | The partner AI plane or its quota-readiness service is disabled. No `Retry-After` is promised for this refusal. |
| 503 | `user_self_deletion_pending` | The API key owner scheduled self-deletion. The access-tokens check refuses the call before its handler. Header `Retry-After: 604800`. |

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

## Known specifics

- **Minting a link is recorded in the Bitrix24 account's access log.** A successful mint in `share-url` mode is logged when the server is bound to a Bitrix24 account, and does not depend on the account's settings. The administrator can read that log and receive notifications in accounts where the platform has enabled the "Applications" section: there the administrator sees that the application was opened via a link, and if the link does not require signing in to Bitrix24 (that is, `identityBound` is `false`) and the application had not previously been exposed externally, administrators additionally receive a chat-bot message with a link to the list of externally accessible applications. The request format, the response and the refusal codes do not change.
- **For tokens issued by the key that manages the server, extending access is cheaper than minting again.** [Token refresh](./refresh.md) issues a fresh JWT for the same record and counts against neither the active-token cap nor the hourly mint limit.
- **For `api-bearer`, the identifier in the log is always the same.** It is the UUID of the API key owner. The `identityBound` field does not affect it and is returned as `true` in the list response.
- **`api-bearer` confirms the request is made by the key owner, but does not create a Bitrix24 user session.** The token authenticates the request as the API key owner and grants access to the application according to its access policy. It does not inject the `X-Vibe-Authorization` header and does not provide `currentUser` in the `GET /v1/me` response. Therefore an application route that verifies a Bitrix24 administrator via `X-Vibe-Authorization` and `GET /v1/me` will get `currentUser: null` under an `api-bearer` token. Such a route needs full user authorization through an OAuth application (`placement`) — see [What the app receives](/docs/infra/app-runtime).

## See also

- [Refresh a token](./refresh.md)
- [List tokens](./list.md)
- [Revoke a token](./delete.md)
- [Access tokens](/docs/infra/access-tokens)
- [Deploy API](/docs/infra/deploy)
