## 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 |
| `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 |
| `enabled` | boolean | no | `true` | Whether the window is active. A disabled window doesn't participate in waking but stays in the list |

The `lead` margin resolves in three tiers: the window's own value, else a server-level margin set apart from this CRUD, else the platform default — currently 180 seconds. For a server with `kind: "GALAXY_APP"`, waking runs in two phases — bringing up the host and starting the container — so budget `lead` up to 900 seconds.

## 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 app

```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 app

```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 during the short window 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` |
| `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 |

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

## 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"`). Nested apps (`kind: "GALAXY_APP"`) are accepted — only the host itself is rejected |
| 400 | `ALWAYS_ON_CONFLICT` | The server runs in always-on mode — no auto-sleep. A wake schedule would silently break that guarantee |
| 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 API key is invalid or expired |
| 403 | `WAKE_SCHEDULE_DISABLED` | Scheduled wake is not enabled for this portal |
| 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 |
| 404 | `NOT_FOUND` | The server was not found, was deleted, or belongs to a different API key |
| 429 | `RATE_LIMITED` | The 10-requests-per-minute limit for this endpoint was exceeded |

Full list of shared error codes — [Errors](/docs/errors).

## Known behavior

- **Check order.** The key's scope and server ownership are checked before the other gates, so `WAKE_SCHEDULE_DISABLED`/`BLACKHOLE_ONLY`/`GALAXY_NOT_SUPPORTED`/`ALWAYS_ON_CONFLICT` only ever surface for a request with a valid key against an owned, existing server. The cron-expression checks (`VALIDATION_ERROR`, then `CADENCE_TOO_LOW`) run after these gates.
- **`tzWarning`/`tzWarningCode` are a conservative heuristic.** The platform doesn't track whether the server was actually 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 lands in `TZ` on the next deploy, `"MULTI_ZONE"` means the windows span more than one zone and `TZ` is not auto-injected.
- **`preemptibleAdvisoryCode`/`preemptibleAdvisory` are an advisory, not a gate.** The window is created or updated regardless of these fields' value — on a preemptible plan the platform does not forbid a schedule (that's its intended use case), it only honestly reports that waking by the window's moment is best-effort. Localize on the `preemptibleAdvisoryCode` code, not on the `preemptibleAdvisory` string — that is a fixed English string for non-UI clients, not meant to be shown to a user as-is.
- **The platform does not configure the cron inside the VM.** `POST /wake-schedules` only guarantees the server is up by the target moment (best-effort on a preemptible plan — see `preemptibleAdvisoryCode` above). Firing the task at that moment is the app's own `cron`/timer's job.

## See also

- [List windows](./list.md)
- [Update a window](./update.md)
- [Delete a window](./delete.md)
- [What the app receives](/docs/infra/app-runtime) — deploy timezone and the in-VM cron
