## Ticket for phone access

`POST /v1/cowork/relay-ticket`

Issues a signed ticket that the Cowork/Code desktop app uses to connect to the relay, the server through which a phone reaches the desktop. The platform only confirms who is connecting: no traffic passes through it, and the desktop is not registered as a server.

The ticket is valid for 5 minutes and is bound to the desktop public key from the request body: the relay rejects it if presented with any other key. Keep the ticket in memory and reuse it for reconnects while more than 60 seconds remain before `expiresAt`. Once 60 seconds or less remain before `expiresAt`, request a new ticket on the next connection.

**Scope:** `vibe:cowork`; the key must also be a Cowork/Code desktop key. The key owner must have an active Cowork/Code seat.

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|-------|----------|
| `pubkey` | string | yes | The Ed25519 public key the desktop presents to the relay: exactly 32 bytes in base64url without `=` padding, 43 characters |

## Examples

This endpoint accepts only a Cowork/Code desktop key, so there are two examples.

### curl — Cowork/Code desktop key

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

### JavaScript — Cowork/Code desktop key

```javascript
let cached = null

async function getRelayTicket(pubkey) {
  // Reuse the ticket while more than 60 seconds remain until expiry
  if (cached && Date.parse(cached.expiresAt) - Date.now() > 60_000) {
    return cached.ticket
  }

  const res = await fetch('https://vibecode.bitrix24.com/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 } — no success/data wrapper
  return cached.ticket
}
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `ticket` | string | A JWS (JSON Web Signature) token in compact serialization, signed with EdDSA. Pass it to the relay as is |
| `expiresAt` | string | When the ticket expires (ISO 8601), 5 minutes after issuance |

For reference: the ticket header carries `alg: "EdDSA"`, `typ: "JWT"` and the `kid` of the signing key. The payload holds `typ: "cowork-relay-ticket"`, `iss: "vibecode-platform"`, `aud: "cowork-relay"`, `sub` and `portal`, `cnf.jwk`, whose `x` field holds the `pubkey` you sent, as well as `jti`, `iat` and `exp`. The `sub` and `portal` fields hold pseudonyms: the relay can tell users and Bitrix24 accounts apart by them but does not learn the real identifiers.

## Response example

The `ticket` value is truncated:

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

## Error response example

403 — a key with the `vibe:cowork` scope that is not a Cowork/Code desktop key:

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

## Errors

| HTTP | Code | Description |
|------|------|-------------|
| 400 | `INVALID_PUBKEY` | The `pubkey` field is missing or is not a 32-byte key in unpadded base64url |
| 400 | `FST_ERR_CTP_EMPTY_JSON_BODY` | The `Content-Type: application/json` header was sent, but the body is empty |
| 400 | `FST_ERR_CTP_INVALID_JSON_BODY` | The body cannot be parsed as JSON |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The key is not recognized — no such key exists on the platform |
| 401 | `KEY_INACTIVE` | The key has been revoked, for example when the device was signed out with [`DELETE /v1/cowork/key`](/docs/cowork/key) |
| 401 | `KEY_EXPIRED` | The key has expired |
| 402 | `ACCOUNT_FROZEN` | The billing account is frozen over a negative balance. This call is not paid for from the balance, so the refusal arrives only while the old freeze rules still apply to your Bitrix24 account. The new rules block only paid calls, and the platform is enabling them gradually |
| 403 | `INSUFFICIENT_SCOPE` | The key lacks the `vibe:cowork` scope |
| 403 | `COWORK_DESKTOP_KEY_REQUIRED` | The key does not belong to the Cowork/Code desktop class. This response applies to both agent seat keys and third-party agent keys |
| 403 | `COWORK_SEAT_INACTIVE` | The Cowork/Code seat exists, but its state is `PAUSED`, `PARKED` or `CANCELLED` rather than `ACTIVE` |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key was issued in read-only mode |
| 404 | `COWORK_NOT_ACTIVATED` | The key owner has no Cowork/Code seat on this Bitrix24 account |
| 413 | `PAYLOAD_TOO_LARGE` | The body is larger than 4 KB |
| 415 | `FST_ERR_CTP_INVALID_MEDIA_TYPE` | The request body uses a content type this route does not parse. Send the body with the `Content-Type: application/json` header |
| 429 | `RATE_LIMITED` | The per-user request limit on the Bitrix24 account has been exceeded; a user's keys share one limit. The platform-wide limit is 30 requests per hour. The effective limit for your key is returned in the `x-ratelimit-limit` header. It is lower than the platform-wide limit because that limit is divided across replicas |
| 503 | `COWORK_FEATURE_DISABLED` | Cowork/Code is disabled at the platform level |
| 503 | `COWORK_RELAY_NOT_CONFIGURED` | Ticket issuance is currently unavailable on this platform |

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

## Known specifics

**A successful response (200) is the object itself, with no `success` wrapper.** Errors come in the `{ success: false, error: { code, message } }` envelope. Determine success by the HTTP status (`res.ok`).

**The ticket is not stored on the platform.** Each call issues a new ticket, and earlier ones stay valid until their `expiresAt`. An issued ticket cannot be revoked.

**The endpoint checks whether ticket issuance is available before it validates the request body.** While ticket issuance is unavailable, the endpoint returns `503 COWORK_RELAY_NOT_CONFIGURED` even for a request with an invalid `pubkey`.

**How to react to refusals.**

- `401`: ask the user to sign in again.
- `400`, `403 INSUFFICIENT_SCOPE` and `403 COWORK_DESKTOP_KEY_REQUIRED`: a retry returns the same refusal, so fix the request or the key.
- `404 COWORK_NOT_ACTIVATED` and `403 COWORK_SEAT_INACTIVE`: the user needs an active Cowork/Code seat.
- `503`: a platform state, not a problem with the key, so retry later.
- `429`: retry with a growing delay.

## See also

- [Cowork/Code](/docs/cowork)
- [Cowork/Code subscription state](/docs/cowork/state)
- [Sign this device out](/docs/cowork/key)
- [Errors](/docs/errors)
