
## 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**. The JWT is valid for up to 10 minutes (or until the record's `expiresAt`, whichever is sooner). A long-running client (CI, AI agent) should call refresh before `jwtExpiresAt` expires rather than minting a new token: refresh does not consume the active-token limit or the hourly mint limit.

No request body is required.

## Parameters

| Parameter | In | Type | Required | Description |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | yes | BLACKHOLE server ID |
| `tokenId` | path | string (UUID) | yes | `api-bearer` token ID (the `data.id` field from the mint response) |

## Examples

### curl

```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"
```

### JavaScript — "use → on 401 refresh → retry" loop

```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 { data } = await r.json()
    jwt = data.token // same tokenId, fresh JWT
    res = await fetch(`${appUrl}/api/health`, {
      headers: { Authorization: `Bearer ${jwt}` },
    })
  }
  return res
}
```

## 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) | Real 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 | Explanation of validity and re-calling |

## Response example

```json
{
  "success": true,
  "data": {
    "id": "tk_7d3fa2b1",
    "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)."
  }
}
```

## 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` | The `X-Api-Key` header was not provided |
| 401 | `INVALID_API_KEY` | Invalid or expired API key |
| 403 | `TOKEN_OWNER_MISMATCH` | The token belongs to a different key, or the server is no longer bound to your key |
| 404 | `NOT_FOUND` | Token not found on this server |
| 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` |
| 503 | `FEATURE_DISABLED` | Access tokens are disabled on the platform |

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 verify | Use a token minted for this server; do not edit it |
| `subdomain` | The token is bound to a different subdomain | Call 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 the token is intact |

## Known specifics

- **Refresh creates no new record.** `data.id` stays the same; the token record and its `expiresAt` do not change — only the JWT is renewed. A single revoke (`DELETE`) kills the original and all refreshed JWTs of one token.
- **A JWT never outlives its record.** If less than 10 minutes remain until the record's `expiresAt`, the new `jwtExpiresAt` is capped to that time.
- **`api-bearer` only.** `share-url` links refresh automatically on each visit via `/auth/bh-login`.

## See also

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