## Билет для телефонного доступа

`POST /v1/cowork/relay-ticket`

Выдаёт десктопу Cowork/Code подписанный билет для подключения к ретранслятору — серверу, через который телефон получает доступ к десктопу. Платформа только подтверждает, кто подключается: трафик через неё не идёт, а десктоп не регистрируется как сервер.

Билет действует 5 минут и привязан к публичному ключу десктопа из тела запроса: с другим ключом ретранслятор его не примет. Держите билет в памяти и используйте его для переподключений, пока до `expiresAt` остаётся больше 60 секунд. Когда до `expiresAt` остаётся 60 секунд или меньше, при следующем подключении запросите новый билет.

**Скоуп:** `vibe:cowork` плюс класс ключа десктопа Cowork/Code. У владельца ключа должно быть активное место Cowork/Code.

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|-------|----------|
| `pubkey` | string | да | Публичный ключ Ed25519, которым десктоп представляется ретранслятору: ровно 32 байта в base64url без знаков `=` в конце — 43 символа |

## Примеры

Эндпоинт принимает только ключ десктопа Cowork/Code, поэтому примеров два.

### curl — ключ десктопа Cowork/Code

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/cowork/relay-ticket \
  -H "X-Api-Key: YOUR_COWORK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pubkey": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}'
```

### JavaScript — ключ десктопа Cowork/Code

```javascript
let cached = null

async function getRelayTicket(pubkey) {
  // Переиспользуем билет, пока до истечения больше 60 секунд
  if (cached && Date.parse(cached.expiresAt) - Date.now() > 60_000) {
    return cached.ticket
  }

  const res = await fetch('https://vibecode.bitrix24.tech/v1/cowork/relay-ticket', {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_COWORK_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ pubkey }),
  })
  if (!res.ok) {
    const { error } = await res.json()
    throw new Error(`${res.status} ${error.code}`)
  }

  cached = await res.json() // { ticket, expiresAt } — без обёртки success/data
  return cached.ticket
}
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `ticket` | string | Токен JWS (JSON Web Signature) в компактной записи с подписью EdDSA. Передавайте его ретранслятору как есть |
| `expiresAt` | string | Момент истечения билета (ISO 8601), через 5 минут после выдачи |

Для справки: заголовок билета несёт `alg: "EdDSA"`, `typ: "JWT"` и `kid` ключа подписи. В полезной нагрузке лежат `typ: "cowork-relay-ticket"`, `iss: "vibecode-platform"`, `aud: "cowork-relay"`, `sub` и `portal`, `cnf.jwk`, в поле `x` которого лежит переданный `pubkey`, а также `jti`, `iat` и `exp`. Поля `sub` и `portal` — псевдонимы: ретранслятор различает по ним пользователей и аккаунты, но настоящих идентификаторов не узнаёт.

## Пример ответа

Значение `ticket` сокращено:

```json
{
  "ticket": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9.eyJ0eXAiOiJjb3dvcmstcmVsYXktdGlja2V0Ii4uLn0.c2lnbmF0dXJl",
  "expiresAt": "2026-09-22T12:05:00.000Z"
}
```

## Пример ответа при ошибке

403 — ключ со скоупом `vibe:cowork`, но не от десктопа Cowork/Code:

```json
{
  "success": false,
  "error": {
    "code": "COWORK_DESKTOP_KEY_REQUIRED",
    "message": "Only a Cowork/Code desktop key may request a relay ticket."
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_PUBKEY` | Поле `pubkey` не передано или не является 32-байтовым ключом в base64url без знаков `=` в конце |
| 400 | `FST_ERR_CTP_EMPTY_JSON_BODY` | Передан заголовок `Content-Type: application/json`, а тело пустое |
| 400 | `FST_ERR_CTP_INVALID_JSON_BODY` | Тело не разбирается как JSON |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не опознан: такой строки на платформе нет |
| 401 | `KEY_INACTIVE` | Ключ отозван — например, устройство отключили через [`DELETE /v1/cowork/key`](/docs/cowork/key) |
| 401 | `KEY_EXPIRED` | Срок действия ключа истёк |
| 402 | `ACCOUNT_FROZEN` | Счёт заморожен из-за отрицательного баланса. Этот вызов с баланса не оплачивается, поэтому отказ приходит, только пока на аккаунте действуют прежние правила заморозки. Новые правила закрывают лишь платные вызовы, платформа включает их постепенно |
| 403 | `INSUFFICIENT_SCOPE` | У ключа нет скоупа `vibe:cowork` |
| 403 | `COWORK_DESKTOP_KEY_REQUIRED` | Ключ не относится к классу десктопа Cowork/Code — такой отказ получает и ключ агентского места, и ключ стороннего агента |
| 403 | `COWORK_SEAT_INACTIVE` | Место Cowork/Code есть, но его состояние не `ACTIVE`, а `PAUSED`, `PARKED` или `CANCELLED` |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ выпущен в режиме «только чтение» |
| 404 | `COWORK_NOT_ACTIVATED` | У владельца ключа нет места Cowork/Code на этом аккаунте |
| 413 | `PAYLOAD_TOO_LARGE` | Тело больше 4 КБ |
| 415 | `FST_ERR_CTP_INVALID_MEDIA_TYPE` | Тело прислано с типом содержимого, который этот маршрут не разбирает. Отправляйте тело с заголовком `Content-Type: application/json` |
| 429 | `RATE_LIMITED` | Превышен лимит запросов одного пользователя в аккаунте — несколько ключей одного пользователя делят общий лимит. Суммарный лимит платформы — 30 запросов в час. Действующее для вашего ключа значение приходит в заголовке `x-ratelimit-limit` — оно ниже суммарного, поскольку лимит делится между репликами |
| 503 | `COWORK_FEATURE_DISABLED` | Cowork/Code отключён на уровне платформы |
| 503 | `COWORK_RELAY_NOT_CONFIGURED` | Выдача билетов сейчас недоступна на этой платформе |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Успешный ответ (200) — это сам объект, без обёртки `success`.** Ошибки приходят в конверте `{ success: false, error: { code, message } }`. Определяйте успех по HTTP-статусу (`res.ok`).

**Билет не сохраняется на платформе.** Каждый вызов выдаёт новый билет, а прежние действуют до своего `expiresAt`. Отозвать выданный билет нельзя.

**Эндпоинт сначала проверяет, доступна ли выдача билетов, и только потом — тело запроса.** Пока выдача билетов недоступна, эндпоинт отвечает `503 COWORK_RELAY_NOT_CONFIGURED` даже на запрос с неверным `pubkey`.

**Как реагировать на отказы.**

- `401` — попросите пользователя войти заново.
- `400`, `403 INSUFFICIENT_SCOPE` и `403 COWORK_DESKTOP_KEY_REQUIRED` — повтор вернёт тот же отказ, исправьте запрос или ключ.
- `404 COWORK_NOT_ACTIVATED` и `403 COWORK_SEAT_INACTIVE` — пользователю нужно активное место Cowork/Code.
- `503` — состояние платформы, а не проблема ключа: повторите позже.
- `429` — повторяйте с растущей паузой.

## Смотрите также

- [Cowork/Code](/docs/cowork)
- [Состояние подписки Cowork/Code](/docs/cowork/state)
- [Отключить это устройство](/docs/cowork/key)
- [Ошибки](/docs/errors)
