
## Выгодные часы в подписке Cowork/Code

В отдельные часы недели квота подписки расходуется медленнее — один и тот же запрос забирает меньшую долю месячного лимита. Ответы подписки показывают, действует ли скидка прямо сейчас и когда она начнётся.

Скидка применяется сама, включать её не нужно. Блок `offPeak` показывает то, что уже учтено при расходе квоты, чтобы приложение могло предложить перенести объёмную задачу на выгодный час.

## Примеры

### curl — личный ключ

```bash
curl https://vibecode.bitrix24.tech/v1/cowork/state \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth-приложение

```bash
curl https://vibecode.bitrix24.tech/v1/cowork/state \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — личный ключ

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/cowork/state', {
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
})

const state = await res.json()
const off = state.offPeak

if (!off || !off.enabled) {
  console.log('Выгодные часы для этой подписки не действуют')
} else if (off.visible === 'now') {
  const discount = Math.round((1 - off.currentMultiplier) * 100)
  console.log(`Квота расходуется на ${discount}% медленнее ещё ${off.currentWindowEndsInHours} ч`)
} else if (off.visible === 'next' && off.nextAtOrBelow) {
  console.log(`Выгодный час начнётся через ${off.nextAtOrBelow.inHours} ч`)
}
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/cowork/state', {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})

const state = await res.json()
```

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

Блок приезжает в двух ответах и различается составом.

| Ответ | Состав блока |
|-------|--------------|
| [`GET /v1/cowork/state`](/docs/cowork/state) | Блок целиком — вместе с сеткой `grid` и полем `touSavedPct` |
| [`GET /v1/cowork/me`](/docs/cowork/me) | Блок без сетки и без `touSavedPct` |

Оба ключа необязательные. Возможность включается для аккаунтов по очереди, и пока она не включена, ключей `offPeak` и `touSavedPct` в теле ответа нет вовсе.

| Поле | Тип | Описание |
|------|-----|---------|
| `offPeak` | object | Блок выгодных часов |
| `offPeak.enabled` | boolean | Действуют ли выгодные часы для этой подписки. При `false` остальные поля пустые, все часы идут по полной цене |
| `offPeak.visible` | string | Что показать пользователю: `now` — скидка действует прямо сейчас, `next` — заметная скидка начнётся в ближайшие часы, `none` — показывать нечего |
| `offPeak.timezone` | string или null | Часовой пояс расписания — идентификатор из базы часовых поясов IANA, например `Europe/Moscow`. По нему считаются день недели и час |
| `offPeak.currentMultiplier` | number | Множитель расхода квоты в текущий час, больше `0` и не больше `1`. Значение `0.5` означает, что квота расходуется вдвое медленнее, значение `1` — без скидки |
| `offPeak.currentWindowEndsInHours` | number или null | Через сколько часов расход перестанет быть таким же выгодным. `null`, если в пределах недели вперёд дороже не станет |
| `offPeak.nextAtOrBelow` | object или null | Ближайший час, скидка в котором достаточна, чтобы её показывать. `null`, когда такого часа нет в пределах недели вперёд |
| `offPeak.nextAtOrBelow.inHours` | number | Через сколько часов наступит этот час |
| `offPeak.nextAtOrBelow.multiplier` | number | Множитель расхода квоты в этом часе |
| `offPeak.perModel` | boolean | `true`, если расписание подобрано под одну модель, а не общее для всех моделей подписки |
| `offPeak.modelName` | string или null | Название модели, под которую подобрано расписание. `null`, когда расписание общее |
| `offPeak.grid` | array или null | Сетка множителей `grid[день][час]`. Семь строк по 24 значения. День `0` — воскресенье, день `6` — суббота. Час — от `0` до `23` в часовом поясе `timezone`. Приходит только в `GET /v1/cowork/state` |
| `offPeak.nowCell` | object или null | Ячейка сетки, которой соответствует текущий момент. Считается на сервере, поэтому совпадает с `currentMultiplier` |
| `offPeak.nowCell.dow` | number | День недели, от `0` (воскресенье) до `6` (суббота) |
| `offPeak.nowCell.hour` | number | Час, от `0` до `23` |
| `touSavedPct` | number или null | Какую долю расхода за текущий расчётный период сняли выгодные часы, в процентах с одним знаком после запятой. Приходит только в `GET /v1/cowork/state` |

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

Ответ `GET /v1/cowork/state`. Показаны блок выгодных часов и доля сэкономленного, остальные поля — в [Состоянии подписки Cowork/Code](/docs/cowork/state).

```json
{
  "offPeak": {
    "enabled": true,
    "visible": "now",
    "timezone": "Europe/Moscow",
    "currentMultiplier": 0.5,
    "currentWindowEndsInHours": 6,
    "nextAtOrBelow": { "inHours": 1, "multiplier": 0.5 },
    "perModel": false,
    "modelName": null,
    "grid": [
      [0.75, 0.87, 0.87, 0.87, 0.87, 0.87, 0.87, 0.87, 0.75, 0.75, 0.75, 0.75, 0.62, 0.5, 0.75, 0.87, 1, 1, 0.87, 0.87, 0.87, 0.75, 0.75, 0.75],
      [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.62, 0.62, 0.62, 0.62, 0.75, 0.75, 0.87, 0.87, 0.75, 0.75, 0.75, 0.75, 0.62, 0.5, 0.5],
      [0.5, 0.5, 0.62, 0.62, 0.75, 0.62, 0.62, 0.62, 0.75, 0.75, 0.87, 1, 1, 0.87, 0.75, 0.87, 0.87, 0.75, 0.75, 0.62, 0.62, 0.62, 0.62, 0.62],
      [0.75, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.62, 0.62, 0.62, 0.75, 0.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.87],
      [1, 1, 1, 1, 1, 0.87, 0.87, 0.87, 0.87, 1, 1, 1, 1, 1, 1, 0.87, 0.75, 0.75, 0.75, 0.87, 1, 1, 1, 1],
      [1, 1, 1, 1, 1, 0.87, 0.87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.87, 0.87, 0.75, 0.87],
      [0.75, 0.62, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.62, 0.75, 0.75, 0.62, 0.62, 0.75, 0.75, 0.87, 0.87, 1, 1, 1, 1, 0.87]
    ],
    "nowCell": { "dow": 1, "hour": 3 }
  },
  "touSavedPct": 12.4
}
```

Тот же момент в ответе `GET /v1/cowork/me` — без сетки и без доли сэкономленного:

```json
{
  "offPeak": {
    "enabled": true,
    "visible": "now",
    "timezone": "Europe/Moscow",
    "currentMultiplier": 0.5,
    "currentWindowEndsInHours": 6,
    "nextAtOrBelow": { "inHours": 1, "multiplier": 0.5 },
    "perModel": false,
    "modelName": null,
    "nowCell": { "dow": 1, "hour": 3 }
  }
}
```

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

404 — подписка не активирована:

```json
{
  "success": false,
  "error": {
    "code": "COWORK_NOT_ACTIVATED",
    "message": "No active Cowork/Code subscription for this user+portal"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный API-ключ |
| 403 | `INSUFFICIENT_SCOPE` | У ключа нет скоупа `vibe:cowork` |
| 404 | `COWORK_NOT_ACTIVATED` | Подписка Cowork/Code не найдена для пары пользователь и портал |
| 500 | `INVALID_TIER_CONFIGURATION` | Конфигурация тарифов на платформе некорректна |
| 503 | `COWORK_FEATURE_DISABLED` | Cowork/Code отключён на уровне платформы |

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

## Подсказка в отказе по исчерпанной квоте

Когда окно квоты подписки исчерпано, [`POST /v1/chat/completions`](/docs/ai/chat/completions) отвечает `402` с кодом `cowork_quota_exhausted`. В теле такого отказа приезжает необязательное поле `error.offPeakHint` — через сколько часов блокировка снимется и какой множитель расхода будет действовать в этот момент. По нему приложение решает, достаточно ли просто подождать.

| Поле | Тип | Описание |
|------|-----|---------|
| `error.offPeakHint` | object | Подсказка о выгодном часе на момент разблокировки |
| `error.offPeakHint.inHours` | number | Через сколько часов снимется блокировка. Округляется вверх — тот же источник, что и у заголовка `Retry-After` |
| `error.offPeakHint.multiplier` | number | Множитель расхода квоты, который будет действовать в этот момент |

```json
{
  "error": {
    "message": "Cowork/Code quota exhausted for the 5h window. Wait until 2026-08-10T15:00:00.000Z or upgrade your tier.",
    "type": "insufficient_quota",
    "code": "cowork_quota_exhausted",
    "window": "5h",
    "resetAt": "2026-08-10T15:00:00.000Z",
    "nextTier": "PRO",
    "offPeakHint": { "inHours": 3, "multiplier": 0.5 }
  }
}
```

Подсказка приходит, только когда возможность включена для аккаунта и момент разблокировки попадает в час со скидкой. В остальных случаях ключа `offPeakHint` в теле нет вовсе — со значением `null` он не приходит, как и остальные поля выгодных часов.

Множитель считается ровно на момент разблокировки, ближайшее выгодное окно при этом не ищется. Если в этот час скидки не будет, подсказки не будет тоже — даже когда выгодный час наступит немного позже.

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

**Проверяйте наличие ключа `offPeak`, а не его значение.** Пока возможность не включена для аккаунта, ключа в теле ответа нет вовсе — он не приходит со значением `null`. Клиент, читающий `state.offPeak.enabled` без проверки самого `offPeak`, упадёт на таком ответе.

**Ветвите интерфейс по `visible`, а не по собственному порогу.** Порог, начиная с которого скидку стоит показывать, платформа решает на своей стороне и наружу не отдаёт. Сравнение `currentMultiplier` с числом, вписанным в клиент, разойдётся с платформой при следующей настройке порога.

**Множитель — коэффициент расхода, а не размер скидки.** Значение `0.62` означает, что вызов забирает 62% от той доли квоты, которую он забрал бы без скидки, то есть расход снижен на 38%. Размер скидки считается как `1 - currentMultiplier`.

**`nextAtOrBelow` ищет заметную скидку, а не любое удешевление.** Этим он отличается от поля `nextWindow` в [Расписании выгодных часов](/docs/ai/consumption/off-peak): там ближайший строго более дешёвый час, здесь — ближайший час, скидка в котором достаточна для показа.

**Блок молчит, пока подписка не активна.** Приостановленная и отменённая подписка получает `enabled: false` — расписание к ней уже не применяется.

**`touSavedPct` считается от расхода, а не от лимита.** Это доля того, что было бы списано без скидки. Значение `null` приходит, когда считать не из чего: расчётный период начался раньше, чем платформа стала вести счёт, либо за период не было расхода.

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

- [Состояние подписки Cowork/Code](/docs/cowork/state)
- [Сводка по подписке Cowork/Code](/docs/cowork/me)
- [Расписание выгодных часов](/docs/ai/consumption/off-peak)
- [Cowork/Code](/docs/cowork)
