
## Refresh an access token

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

Mints a fresh JWT for an existing `api-bearer` token — **without creating a new record**. A long-running client (CI, AI agent) calls refresh before `jwtExpiresAt` is reached instead of minting a new token: refresh counts against neither the active-token limit nor the hourly mint limit.

No request body is required.

Refresh is available only with the key that currently manages the server and issued this token. A development-team key cannot call this endpoint; issuing replacement tokens requires separate approval. Switching to the managing key does not make a team-issued token refreshable.

## Parameters

| Parameter | In | Type | Required | Description |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | yes | BLACKHOLE server ID. List: [`GET /v1/infra/servers`](/docs/infra/servers/list) |
| `tokenId` | path | string (UUID) | yes | `api-bearer` token ID — the `data.id` field from a [mint](./create.md) response or an item from the [token list](./list.md) |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/access-tokens/TOKEN_ID/refresh" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/access-tokens/TOKEN_ID/refresh" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/access-tokens/${tokenId}/refresh`,
  {
    method: 'POST',
    headers: { 'X-Api-Key': 'YOUR_API_KEY' },
  }
)
const result = await res.json()
if (!res.ok || result.success !== true) {
  throw new Error(result.error?.code ?? `HTTP_${res.status}`)
}
const { data } = result
console.log(data.token, data.jwtExpiresAt)
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/access-tokens/${tokenId}/refresh`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
const result = await res.json()
if (!res.ok || result.success !== true) {
  throw new Error(result.error?.code ?? `HTTP_${res.status}`)
}
const { data } = result
console.log(data.token, data.jwtExpiresAt)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.id` | string | Token ID — **the same** as at mint time (no new record is created) |
| `data.mode` | string | `"api-bearer"` |
| `data.token` | string | Fresh JWT for the `Authorization: Bearer` header |
| `data.expiresAt` | string (ISO 8601) | Retention period of the token record — unchanged by refresh |
| `data.jwtExpiresAt` | string (ISO 8601) | Actual expiry of the new JWT. Capped at 10 minutes (or the record's `expiresAt`, whichever is sooner) |
| `data.subdomain` | string | Server subdomain |
| `data.appUrl` | string | Full HTTPS address of the app |
| `data.curlExample` | string | Ready-to-use `curl` example with the new token |
| `data.note` | string | Note about the validity periods and when to call refresh again |

## Response example

```json
{
  "success": true,
  "data": {
    "id": "9f1c4b7e-3d52-4a18-9c0e-7b2a1f6d84c3",
    "mode": "api-bearer",
    "token": "eyJhbGciOiJFUzI1NiJ9...",
    "expiresAt": "2026-06-25T10:50:00.000Z",
    "jwtExpiresAt": "2026-06-25T10: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": "Refreshed the Gateway session JWT for this api-bearer token (same token id, no new row). The JWT is valid for up to 10 minutes (or until the row's expiresAt, whichever is sooner). Call this endpoint again before jwtExpiresAt to keep a long-running client authenticated."
  }
}
```

## Error response example

410 — the record's retention period has elapsed:

```json
{
  "success": false,
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "The long-lived token row has expired; mint a new token."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `WRONG_TOKEN_MODE` | The token is in `share-url` mode. Refresh applies only to `api-bearer`; `share-url` links auto-refresh on each visit |
| 400 | `SERVER_NO_SUBDOMAIN` | The server has no subdomain |
| 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 | `KEY_BLOCKED` | The current or a recorded previous token issuer is blocked |
| 403 | `TOKEN_OWNER_MISMATCH` | The token belongs to a different key, or the server is no longer bound to your key |
| 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 | `PORTAL_COLLABORATOR_KEY_OUT_OF_SCOPE` | This operation is outside the development-team key route list. Use the key that manages the server |
| 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 | `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 token was not found on this server or the server's portal has been deactivated |
| 404 | `SERVER_NOT_FOUND` | Server not found or deleted |
| 410 | `ALREADY_REVOKED` | The token was revoked — mint a new one |
| 410 | `TOKEN_EXPIRED` | The record's retention period has expired — mint a new token via `POST /access-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 key or user request limit. Wait for the number of seconds specified in `Retry-After`; `X-RateLimit-Scope` identifies the refusing layer |
| 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 token plugin refuses the call before its handler. Header `Retry-After: 604800`. |

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

## Diagnosing a Gateway rejection

If a request to the app with the `Authorization: Bearer` header returns `401` with code `BH_LOGIN_REQUIRED`, the response body carries a `reason` field with the specific Gateway rejection cause:

| `reason` | What happened | What to do |
|----------|---------------|------------|
| `expired` | The JWT expired (the 10-minute window elapsed) | Refresh the token via this endpoint, or mint a new one |
| `signature` | The signature did not match | Use a token minted for this server; do not edit it |
| `subdomain` | The token is bound to a different subdomain | Send the request to the server's own `app-*` subdomain |
| `revoked` | The token was revoked | Mint a new token |
| `type` | A non-`api-bearer` token was sent | Use an `api-bearer` token, not a cookie session |
| `malformed` / `invalid` | The string is not a valid JWT | Check that the token is intact |

## Known specifics

- **A single revocation invalidates every JWT issued for the token.** A [`DELETE`](./delete.md) on the record stops both the original JWT and every JWT issued by refresh.
- **`share-url` links refresh themselves.** Every visit goes through `/auth/bh-login`, so calling refresh for them is unnecessary.
- **A ready-made "use → on 401 refresh → retry" loop.** Instead of watching a timer, the client reacts to a rejection:

```javascript
async function callWithRefresh(serverId, tokenId, appUrl, jwt) {
  let res = await fetch(`${appUrl}/api/health`, {
    headers: { Authorization: `Bearer ${jwt}` },
  })
  if (res.status === 401) {
    const r = await fetch(
      `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/access-tokens/${tokenId}/refresh`,
      { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY' } },
    )
    const result = await r.json()
    if (!r.ok || result.success !== true) {
      throw new Error(result.error?.code ?? `HTTP_${r.status}`)
    }
    const { data } = result
    jwt = data.token // same tokenId, fresh JWT
    res = await fetch(`${appUrl}/api/health`, {
      headers: { Authorization: `Bearer ${jwt}` },
    })
  }
  return res
}
```

## See also

- [Mint a token](./create.md)
- [List tokens](./list.md)
- [Revoke a token](./delete.md)
- [Access tokens](/docs/infra/access-tokens)
