## Create a wake window

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

Declares a new recurring wake window for a server in BLACKHOLE mode. The body is a flat object, with no `fields` wrapper.

## Parameters

| Parameter | In | Type | Required | Description |
|-----------|----|------|:--------:|--------------|
| `id` | path | string (UUID) | yes | ID of the BLACKHOLE server |

## Request fields (body)

| Field | Type | Required | Default | Description |
|-------|------|:--------:|---------|--------------|
| `cronExpr` | string | yes | — | A 5-field cron expression (minute hour day-of-month month day-of-week), 9 to 120 characters |
| `timezone` | string | yes | — | IANA timezone of the window, e.g. `America/New_York`. Checked against `Intl.supportedValuesOf('timeZone')` — an unknown value is rejected at validation time |
| `label` | string | no | — | Display label for the window, up to 80 characters |
| `lead` | number | no | the server's margin, else the platform's | Margin, in seconds, before the `cronExpr` moment by which the server must already be up — covers VM boot time. 0 to 3600. A value below the platform margin is raised to it |
| `enabled` | boolean | no | `true` | Whether the window is active. A disabled window never triggers a wake but stays in the list |

The `lead` margin resolves in three tiers: the window's own value, else a server-level margin set outside this CRUD, else the platform margin. The platform margin is also a floor: a smaller value is stored on the window as sent, but the wake still starts no later than the platform margin before the `cronExpr` moment. For a server with `kind: "GALAXY_APP"`, the platform margin is larger than for a standalone server: waking runs in two phases — bringing up the host and starting the container.

Creating a window requires a numeric auto-sleep setting. If the server stores `sleepAfterMinutes: null` (“Never”), the request is rejected as an always-on conflict even when `enabled` is `false`. In the reverse direction, setting `null` is rejected only while an enabled window exists and the server is not in the `SCHEDULE` run mode; an already stored disabled window neither affects the timeout nor blocks that sleep-setting change. An explicit numeric value remains the effective timeout and is not shortened by the schedule. Existing older records are not rewritten: if one already contains `null` plus an effective schedule, the platform's post-window fallback applies — 15 minutes of inactivity by default. Creating or updating a window with `null` is not rejected for an app in a galaxy: the app does not choose its plan but inherits it from the galaxy. Nor is it rejected for a server in the `SCHEDULE` [run mode](/docs/infra/lifecycle/run-mode): its sleep is declared by the schedule.

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/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": "America/New_York",
    "label": "daily report",
    "enabled": true
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/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": "America/New_York",
    "label": "daily report",
    "enabled": true
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/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: 'America/New_York',
      label: 'daily report',
      enabled: true,
    }),
  }
)
const body = await res.json()
if (body.tzWarningCode === 'MULTI_ZONE') {
  console.warn('Windows span more than one timezone — TZ is not injected at deploy time')
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/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: 'America/New_York',
      label: 'daily report',
      enabled: true,
    }),
  }
)
```

## Response fields

| Field | Type | Description |
|-------|------|--------------|
| `success` | boolean | Always `true` on success |
| `data.id` | string | Window ID for a later update or delete |
| `data.serverId` | string (UUID) | ID of the owning server |
| `data.cronExpr` | string | The stored cron expression |
| `data.timezone` | string | The stored timezone |
| `data.label` | string \| null | Window label |
| `data.lead` | number \| null | Margin in seconds. `null` when not set on the window itself |
| `data.enabled` | boolean | Whether the window is active |
| `data.source` | string | How the window was created. Always `"MANUAL"` for windows created through this CRUD |
| `data.lastFiredAt` | string (ISO 8601) \| null | Timestamp of the last confirmed firing. `null` for a freshly created window |
| `data.wakeAttemptStartedAt` | string (ISO 8601) \| null | Internal scheduler claim marker — non-null only for the short interval while a wake is in flight. Normally `null` |
| `data.lastWakeLateAt` | string (ISO 8601) \| null | Timestamp of the last wake that fired later than its computed window. `null` if there have been no late wakes |
| `data.createdAt` | string (ISO 8601) | Record creation date |
| `data.updatedAt` | string (ISO 8601) | Date of the last update to the record |
| `tzWarning` | string \| null | Timezone-mismatch advisory, as text. `null` when the server has no active windows left after this mutation |
| `tzWarningCode` | string \| null | Machine-readable code for the same advisory: `"SINGLE_ZONE"`, `"MULTI_ZONE"`, or `null` |
| `preemptibleAdvisoryCode` | string \| null | `"PREEMPTIBLE_BEST_EFFORT"` when the server runs on a preemptible plan — otherwise `null`. The `.com` plans are non-preemptible, so a server on `.com` gets `null` |
| `preemptibleAdvisory` | string \| null | An explanation of the same advisory, as text (in English, for non-UI API clients). `null` when the server is on a non-preemptible plan — as every `.com` plan is |

`tzWarning`, `tzWarningCode`, `preemptibleAdvisoryCode`, and `preemptibleAdvisory` are top-level response fields, not nested under `data`.

## Response example

The server's only active window — the timezone is unambiguous:

```json
{
  "success": true,
  "data": {
    "id": "cm38x02qp0001ml08g7k3h2a",
    "serverId": "e765edfc-ba0a-43de-b8ea-838dd872c522",
    "cronExpr": "0 9 * * 1-5",
    "timezone": "America/New_York",
    "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
}
```

A server on a preemptible plan would get `"preemptibleAdvisoryCode": "PREEMPTIBLE_BEST_EFFORT"` and a non-empty `preemptibleAdvisory` instead of `null` in those same two fields — the rest of the response shape is unchanged. The `.com` plans are non-preemptible, so on `.com` both fields are `null`, as in the example above.

## Error response example

400 — the window conflicts with an always-on server:

```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."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|------|--------------|
| 400 | `VALIDATION_ERROR` | Body validation failed — missing `cronExpr`/`timezone`, `timezone` not in the IANA timezone list, `label` over 80 characters, `lead` outside 0–3600, or an unknown field in the body |
| 400 | `BLACKHOLE_ONLY` | The server is not in BLACKHOLE mode |
| 400 | `GALAXY_NOT_SUPPORTED` | The server is a Galaxy host (`kind: "GALAXY"`). A host holds no windows at all: the schedule is declared on an app inside the galaxy, not on the host itself |
| 400 | `ALWAYS_ON_CONFLICT` | The server runs in always-on mode — no auto-sleep. A wake schedule would silently break that guarantee. An app in a galaxy never receives this code: it inherits the galaxy's plan, and the platform does not treat it as always-on. Nor does a server in the `SCHEDULE` [run mode](/docs/infra/lifecycle/run-mode): its owner has declared sleep explicitly, and the window becomes an extra wake-up on top of the schedule |
| 400 | `CADENCE_TOO_LOW` | The gap between consecutive `cronExpr` occurrences is below the platform's floor — currently 5 minutes |
| 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 |
| 403 | `WAKE_SCHEDULE_DISABLED` | Scheduled wake is not enabled for this portal |
| 403 | `WAKE_SCHEDULE_GALAXY_DISABLED` | The server is an app inside a galaxy (`kind: "GALAXY_APP"`). Such an app can hold windows, but the capability for Galaxy apps is switched off on the platform. The capability is enabled separately from ordinary servers, so a `kind: "STANDALONE"` server never receives this code |
| 403 | `WAKE_SCHEDULE_LIMIT` | The server already has 50 windows — the per-server cap |
| 403 | `INFRA_SCOPE_REQUIRED` | The key lacks the `vibe:infra` scope |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | The call was made with a Cowork key — such a key works with data only and cannot perform write operations. To issue a key that can, see [Project key for deploy](/docs/cowork/deploy-key) |
| 403 | `SERVER_ROLE_FORBIDDEN` | You are on this server's development team with the Developer role, and this operation is open to the Administrator role. `error.hint` carries your role, the required threshold and the list of calls that are open to you. Role breakdown — [List servers](/docs/infra/servers/list) |
| 404 | `NOT_FOUND` | The server was not found, was deleted, or belongs to a different API key while you are not on its development team |
| 429 | `RATE_LIMITED` | The 10-requests-per-minute limit for this endpoint was exceeded. The current limit is returned in the `x-ratelimit-limit` header |

For the full list of shared API error codes, see [Errors](/docs/errors).

## Known specifics

- **Check order.** The key's scope and server ownership are checked before the other conditions, so `WAKE_SCHEDULE_DISABLED`/`BLACKHOLE_ONLY`/`GALAXY_NOT_SUPPORTED`/`WAKE_SCHEDULE_GALAXY_DISABLED`/`ALWAYS_ON_CONFLICT` only ever surface for a request with a valid key against an owned, existing server, and among themselves these conditions are checked in the order listed. The cron-expression checks (`VALIDATION_ERROR`, then `CADENCE_TOO_LOW`) run after these gates.
- **Two different galaxy refusals.** `GALAXY_NOT_SUPPORTED` is about the galaxy host: it holds no windows at all, and enabling the capability will not change that. `WAKE_SCHEDULE_GALAXY_DISABLED` is about an app inside a galaxy: it can hold windows, but the capability for such apps is switched off on the platform. The first arrives with status `400`, the second with `403`, so branch on the response code.
- **`tzWarning`/`tzWarningCode` are a conservative heuristic.** The platform does not track whether the application was redeployed since a window was declared, so the advisory fires for any set of active windows after a mutation — even when the timezone is already correctly set in the environment. Go by the code: `"SINGLE_ZONE"` means the timezone is unambiguous and is set in `TZ` on the next deployment of a standalone virtual machine. `"MULTI_ZONE"` means the windows span more than one zone and `TZ` is not injected automatically.
- **`preemptibleAdvisoryCode`/`preemptibleAdvisory` are an advisory, not a gate.** The window is created or updated regardless of these fields' values — on a preemptible plan the platform does not forbid a schedule (that is its intended use case); it only states up front that waking the server in time for the window is best-effort. Localize by `preemptibleAdvisoryCode`, not by the `preemptibleAdvisory` string — that is a fixed English string for non-UI clients, not meant to be shown to a user as-is.
- **`null` in the sleep setting normally blocks schedule creation and update.** It is the stored “Never” choice and disables idle auto-sleep; creating or updating a window while it is stored is rejected as an always-on conflict, even when the submitted window is disabled. Setting `null` through the sleep endpoint is narrower: that write is rejected only while an enabled window exists and the server is not in the `SCHEDULE` run mode. If an older record already contains `null` plus an effective schedule, the platform's post-window timeout applies between windows. V1 does not return the computed session policy: for long-running background work, keep `null` with no enabled windows or choose a numeric timeout long enough for the task. Creating or updating a window with `null` is not rejected for an app in a galaxy: the app does not choose its plan but inherits it from the galaxy. Nor is it rejected for a server in the `SCHEDULE` [run mode](/docs/infra/lifecycle/run-mode): its sleep is declared by the schedule.
- **The platform does not configure the cron inside the VM.** `POST /wake-schedules` only brings the server up for the target moment — ahead of time, with a margin for boot, but with no guarantee it is up in time. On a preemptible plan the window can be skipped entirely — see `preemptibleAdvisoryCode` above. Firing the task at that moment is the job of the app's own `cron`/timer.

## See also

- [List windows](./list.md)
- [Update a window](./update.md)
- [Delete a window](./delete.md)
- [What the app receives](/docs/infra/app-runtime)
