## Создать окно пробуждения

`POST /v1/infra/servers/:id/wake-schedules`

Объявляет новое повторяющееся окно пробуждения для сервера в режиме BLACKHOLE. Тело передаётся плоским объектом, без обёртки `fields`.

## Параметры

| Параметр | В | Тип | Обяз. | Описание |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | да | ID BLACKHOLE-сервера |

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

| Поле | Тип | Обяз. | По умолч. | Описание |
|------|-----|:-----:|-----------|----------|
| `cronExpr` | string | да | — | 5-польное cron-выражение (минута час день-месяца месяц день-недели), от 9 до 120 символов |
| `timezone` | string | да | — | IANA-таймзона окна, например `Europe/Moscow`. Проверяется по списку `Intl.supportedValuesOf('timeZone')` — незнакомое значение отклоняется на этапе валидации |
| `label` | string | нет | — | Метка окна для отображения, до 80 символов |
| `lead` | number | нет | запас сервера, иначе запас платформы | Запас в секундах перед моментом `cronExpr`, на который сервер должен быть уже поднят — покрывает время старта виртуальной машины. От 0 до 3600 |
| `enabled` | boolean | нет | `true` | Активно ли окно. Отключённое окно не участвует в пробуждении, но остаётся в списке |

Запас `lead` разрешает три уровня: значение самого окна, иначе запас сервера, заданный отдельно от этого раздела CRUD, иначе запас по умолчанию на платформе — сейчас 180 секунд. Для сервера `kind: "GALAXY_APP"` пробуждение проходит два этапа — подъём хоста и старт контейнера, поэтому закладывайте `lead` до 900 секунд.

## Примеры

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

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/wake-schedules" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cronExpr": "0 9 * * 1-5",
    "timezone": "Europe/Moscow",
    "label": "daily report",
    "enabled": true
  }'
```

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

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/wake-schedules" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "cronExpr": "0 9 * * 1-5",
    "timezone": "Europe/Moscow",
    "label": "daily report",
    "enabled": true
  }'
```

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

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/wake-schedules`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      cronExpr: '0 9 * * 1-5',
      timezone: 'Europe/Moscow',
      label: 'daily report',
      enabled: true,
    }),
  }
)
const body = await res.json()
if (body.tzWarningCode === 'MULTI_ZONE') {
  console.warn('Окна расходятся по часовым поясам — TZ в деплое не проставляется')
}
```

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

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/wake-schedules`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      cronExpr: '0 9 * * 1-5',
      timezone: 'Europe/Moscow',
      label: 'daily report',
      enabled: true,
    }),
  }
)
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при успехе |
| `data.id` | string | ID окна для последующего обновления и удаления |
| `data.serverId` | string (UUID) | ID сервера-владельца |
| `data.cronExpr` | string | Сохранённое cron-выражение |
| `data.timezone` | string | Сохранённая таймзона |
| `data.label` | string \| null | Метка окна |
| `data.lead` | number \| null | Запас в секундах. `null`, если не задан на уровне окна |
| `data.enabled` | boolean | Активно ли окно |
| `data.source` | string | Как создано окно. Всегда `"MANUAL"` для окон, созданных через этот CRUD |
| `data.lastFiredAt` | string (ISO 8601) \| null | Момент последнего подтверждённого срабатывания. `null` для только что созданного окна |
| `data.wakeAttemptStartedAt` | string (ISO 8601) \| null | Служебная метка планировщика — непусто только в течение короткого окна, пока идёт пробуждение. В норме `null` |
| `data.lastWakeLateAt` | string (ISO 8601) \| null | Момент последнего пробуждения, сработавшего позже расчётного окна. `null`, если опозданий не было |
| `data.createdAt` | string (ISO 8601) | Дата создания записи |
| `data.updatedAt` | string (ISO 8601) | Дата последнего обновления записи |
| `tzWarning` | string \| null | Предупреждение о рассинхроне таймзоны — текстом. `null`, если у сервера не осталось активных окон после этой мутации |
| `tzWarningCode` | string \| null | Машиночитаемый код того же предупреждения: `"SINGLE_ZONE"`, `"MULTI_ZONE"` или `null` |
| `preemptibleAdvisoryCode` | string \| null | `"PREEMPTIBLE_BEST_EFFORT"`, если сервер работает на вытесняемом тарифе — иначе `null` |
| `preemptibleAdvisory` | string \| null | Пояснение того же предупреждения текстом (на английском, для не-UI-клиентов API). `null`, если сервер на невытесняемом тарифе |

`tzWarning`, `tzWarningCode`, `preemptibleAdvisoryCode` и `preemptibleAdvisory` — поля верхнего уровня ответа, не вложены в `data`.

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

Единственное активное окно сервера — таймзона однозначна:

```json
{
  "success": true,
  "data": {
    "id": "cm38x02qp0001ml08g7k3h2a",
    "serverId": "e765edfc-ba0a-43de-b8ea-838dd872c522",
    "cronExpr": "0 9 * * 1-5",
    "timezone": "Europe/Moscow",
    "label": "daily report",
    "lead": null,
    "enabled": true,
    "source": "MANUAL",
    "lastFiredAt": null,
    "wakeAttemptStartedAt": null,
    "lastWakeLateAt": null,
    "createdAt": "2026-07-10T08:12:00.000Z",
    "updatedAt": "2026-07-10T08:12:00.000Z"
  },
  "tzWarning": "The VM may have been deployed with a different timezone (or not redeployed since this window was declared) — verify the in-VM cron or redeploy the app so TZ is re-injected.",
  "tzWarningCode": "SINGLE_ZONE",
  "preemptibleAdvisoryCode": null,
  "preemptibleAdvisory": null
}
```

Сервер на вытесняемом тарифе получил бы `"preemptibleAdvisoryCode": "PREEMPTIBLE_BEST_EFFORT"` и непустой `preemptibleAdvisory` вместо `null` в тех же двух полях — остальная форма ответа не меняется.

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

400 — окно нарушает гарантию сервера «Всегда онлайн»:

```json
{
  "success": false,
  "error": {
    "code": "ALWAYS_ON_CONFLICT",
    "message": "Scheduled wake conflicts with an always-on (24/7) server, which never auto-sleeps. Turn off the always-on toggle, or set a sleep timeout instead of \"Never\", to schedule wake windows."
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Нарушена валидация тела — отсутствует `cronExpr`/`timezone`, `timezone` не входит в список IANA-таймзон, `label` длиннее 80 символов, `lead` вне диапазона 0–3600 или в теле лишнее поле |
| 400 | `BLACKHOLE_ONLY` | Сервер не в режиме BLACKHOLE |
| 400 | `GALAXY_NOT_SUPPORTED` | Сервер — хост галактики (`kind: "GALAXY"`). Вложенные приложения (`kind: "GALAXY_APP"`) принимаются, отклоняется только сам хост |
| 400 | `ALWAYS_ON_CONFLICT` | Сервер работает в режиме «Всегда онлайн» — на невытесняемом тарифе и без авто-сна. Расписание пробуждения тихо нарушило бы эту гарантию |
| 400 | `CADENCE_TOO_LOW` | Интервал между соседними срабатываниями `cronExpr` меньше минимального, заданного на платформе — сейчас 5 минут |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 403 | `WAKE_SCHEDULE_DISABLED` | Пробуждение по расписанию не включено для этого портала |
| 403 | `WAKE_SCHEDULE_LIMIT` | У сервера уже 50 окон — предел на сервер |
| 403 | `INFRA_SCOPE_REQUIRED` | У ключа нет скоупа `vibe:infra` |
| 404 | `NOT_FOUND` | Сервер не найден, удалён или принадлежит другому API-ключу |
| 429 | `RATE_LIMITED` | Превышен лимит 10 запросов в минуту на этот эндпоинт |

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

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

- **Порядок проверок.** Скоуп ключа и владение сервером проверяются раньше остальных гейтов, поэтому `WAKE_SCHEDULE_DISABLED`/`BLACKHOLE_ONLY`/`GALAXY_NOT_SUPPORTED`/`ALWAYS_ON_CONFLICT` увидит только запрос с корректным ключом на существующий свой сервер. Проверка cron-выражения (`VALIDATION_ERROR`, затем `CADENCE_TOO_LOW`) идёт после этих гейтов.
- **`tzWarning`/`tzWarningCode` — консервативная эвристика.** Платформа не хранит, был ли сервер передеплоен после объявления окна, поэтому предупреждение приходит при любом наборе активных окон после мутации — даже если таймзона уже верно проставлена в переменных окружения. Ориентируйтесь на код: `"SINGLE_ZONE"` — таймзона однозначна и попадёт в `TZ` при следующем деплое, `"MULTI_ZONE"` — окна в разных поясах, `TZ` не проставляется автоматически.
- **`preemptibleAdvisoryCode`/`preemptibleAdvisory` — не гейт, а предупреждение.** Окно создаётся или обновляется независимо от значения этих полей — на вытесняемом тарифе платформа не запрещает расписание (это её целевой сценарий), а лишь честно сообщает, что подъём к моменту окна best-effort. Локализуйте по коду `preemptibleAdvisoryCode`, а не по строке `preemptibleAdvisory` — это фиксированная английская строка для не-UI-клиентов, не предназначенная для показа пользователю как есть.
- **Cron внутри VM платформа не настраивает.** `POST /wake-schedules` только гарантирует, что сервер поднят к нужному моменту (на вытесняемом тарифе — best-effort, см. `preemptibleAdvisoryCode` выше). Запуск задачи в этот момент — ответственность собственного `cron`/таймера внутри приложения.

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

- [Список окон](./list.md)
- [Обновить окно](./update.md)
- [Удалить окно](./delete.md)
- [Что приходит в приложение](/docs/infra/app-runtime) — таймзона деплоя и in-VM cron
