
## Расписание выгодных часов

`GET /v1/off-peak`

Возвращает расписание выгодных часов: насколько медленнее расходуется AI-квота компании в каждый час недели и когда наступит ближайшее окно со скидкой.

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

## Параметры

| Параметр | Тип | Обяз. | По умолч. | Описание |
|----------|-----|:-----:|-----------|---------|
| `model` (query) | string | нет | — | Идентификатор модели из [`GET /v1/models`](/docs/ai/models/list). Возвращает расписание этой модели. Без параметра возвращается расписание, отмеченное платформой как основное |

## Примеры

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

```bash
curl "https://vibecode.bitrix24.tech/v1/off-peak?model=bitrix/bitrixgpt-5.5" \
  -H "X-Api-Key: YOUR_API_KEY"
```

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

```bash
curl "https://vibecode.bitrix24.tech/v1/off-peak?model=bitrix/bitrixgpt-5.5" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

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

```javascript
const url = 'https://vibecode.bitrix24.tech/v1/off-peak?model=bitrix/bitrixgpt-5.5'
const res = await fetch(url, {
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
})

const schedule = await res.json()

if (schedule.enabled && schedule.currentMultiplier < 1) {
  const discount = Math.round((1 - schedule.currentMultiplier) * 100)
  console.log(`Сейчас выгодный час: квота расходуется на ${discount}% медленнее`)
} else if (schedule.nextWindow) {
  console.log(`Дешевле станет через ${schedule.nextWindow.inHours} ч`)
}
```

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

```javascript
const url = 'https://vibecode.bitrix24.tech/v1/off-peak?model=bitrix/bitrixgpt-5.5'
const res = await fetch(url, {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})

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

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

Успешный ответ — сам объект расписания, без обёртки `success` и `data`.

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

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

Расписание действует:

```json
{
  "enabled": true,
  "timezone": "Europe/Moscow",
  "currentMultiplier": 1,
  "nextWindow": { "inHours": 4, "multiplier": 0.87 },
  "currentWindowEndsInHours": 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": 4, "hour": 11 }
}
```

Расписание не действует — у модели его нет, либо выгодные часы отключены на платформе:

```json
{
  "enabled": false,
  "timezone": null,
  "currentMultiplier": 1,
  "nextWindow": null,
  "currentWindowEndsInHours": null,
  "grid": null,
  "nowCell": null
}
```

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

`401 MISSING_API_KEY` — не передан заголовок `X-Api-Key`:

```json
{
  "success": false,
  "error": {
    "code": "MISSING_API_KEY",
    "message": "API key required. Pass via X-Api-Key header."
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|---------|
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не найден или отозван |
| 403 | `scope_missing` | API-ключу не хватает скоупа `vibe:ai` |
| 429 | `rate_limit_exceeded` | Превышен лимит запросов к AI-эндпоинтам. Время до сброса — в заголовке `Retry-After` |

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

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

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

**Указывайте модель.** Без параметра `model` эндпоинт отдаёт расписание, отмеченное платформой как основное. Пока основное расписание не назначено, такой вызов возвращает `enabled: false`, даже если у отдельных моделей скидки действуют.

**Неизвестная модель не даёт ошибки.** Если у модели нет расписания или идентификатор не существует, приходит `200` с `enabled: false` и пустыми полями. Признак наличия скидок — поле `enabled`, а не код ответа.

**Расписание меняется.** Платформа пересчитывает сетку по фактической нагрузке, поэтому перечитывайте расписание перед планированием, а не сохраняйте его надолго.

**`nextWindow` ищет строго более дешёвый час.** Если текущий час уже самый дешёвый в пределах недели вперёд, поле равно `null`. Это не означает отсутствия скидки — смотрите `currentMultiplier`.

**Скидка замедляет заполнение окон равномерного расходования.** Суточное и недельное окна растут на ту же величину расхода, что и месячная квота, то есть уже со скидкой. Перенос отложенных задач в выгодные часы отдаляет и `429 ai_pacing_limited`, и исчерпание месячной квоты.

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

- [AI-квота компании](/docs/ai/consumption/quota)
- [Список моделей](/docs/ai/models/list)
- [Чат-комплишены](/docs/ai/chat/completions)
- [AI Router](/docs/ai)
