---
title: "Schedules"
description: "List and create Incident Management on-call schedules: the rotation that decides who is on call, and when, for a team."
---

`GET /api/im/schedules` · `POST /api/im/schedules`

A **schedule** belongs to exactly one team and defines its on-call rotation: one or more layers of users, each rotating on a `daily`, `weekly`, or `custom` cadence, optionally restricted to specific times of day. A background worker materializes the rotation into concrete shifts (`im_schedule_shift`) roughly 90 days ahead, which is what [Who Is On Call](./on-call) and the escalation engine actually read at runtime. This endpoint manages the rotation's *definition*, not the materialized shifts directly.

## Authentication

Requires the base IM access every endpoint in this API needs (an IM-eligible role, or an organization-wide API token; Incident Management must be enabled for the organization). Listing is available to any IM-eligible role. **Creating** a schedule additionally requires the write bar: your role must be `admin`, or you must be a team admin of the schedule's team. An organization-wide API token satisfies the organization-admin bar.

## List schedules

`GET /api/im/schedules`

Returns every schedule in your organization, with its team's name.

### Example (cURL)

```bash
curl -X GET "$BASE_URL/api/im/schedules" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"
```

### Response

`200 OK`: an array, ordered by name.

```json
[
  {
    "id": 7,
    "organizationId": 1,
    "teamId": 3,
    "teamName": "Platform Team",
    "name": "Primary On-Call",
    "timezone": "Europe/Berlin",
    "rotation": [
      {
        "users": ["u_abc123", "u_def456"],
        "type": "weekly",
        "handoverTime": "09:00",
        "startDate": "2026-01-05"
      }
    ],
    "createdAt": "2026-01-01T00:00:00.000Z",
    "updatedAt": "2026-01-01T00:00:00.000Z"
  }
]
```

## Create a schedule

`POST /api/im/schedules`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|--------------|
| `teamId` | number | Yes | The team the schedule belongs to. Must belong to your organization. |
| `name` | string | Yes | Schedule name, up to 120 characters. |
| `timezone` | string | Yes | IANA timezone (e.g. `Europe/Berlin`). Every handover and time-of-day restriction is evaluated in this zone, across DST. |
| `rotation` | array | No | Rotation layers, up to 20. Defaults to an empty array (a schedule with no rotation pages nobody until layers are added). Each layer: `{ users: string[], type: 'daily' \| 'weekly' \| 'custom', intervalDays?: number, handoverTime: "HH:mm", startDate: "yyyy-MM-dd", restrictions?: Array<{ dow: number[], from: "HH:mm", to: "HH:mm" }> }`. `intervalDays` is required (and only meaningful) when `type` is `custom`. `dow` in `restrictions` uses ISO weekdays (1 = Monday .. 7 = Sunday), up to 30 restrictions per layer. Up to 200 users per layer. |

Every user ID in `rotation[].users` must belong to your organization. A rotation naming a user outside it is rejected outright, not silently dropped, since `im_schedule_shift.user_id` has no organization boundary of its own.

### Example (cURL)

```bash
curl -X POST "$BASE_URL/api/im/schedules" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "teamId": 3,
    "name": "Primary On-Call",
    "timezone": "Europe/Berlin",
    "rotation": [
      {
        "users": ["u_abc123", "u_def456"],
        "type": "weekly",
        "handoverTime": "09:00",
        "startDate": "2026-01-05"
      }
    ]
  }'
```

### Response

`200 OK`: the newly created schedule row (same shape as the list above, without `teamName`).

## Common errors

- `401 Unauthorized` when not authenticated
- `403 Forbidden` (`imAccessDenied`) when using a customer-scoped token, or a session without an IM-eligible role
- `403 Forbidden` (`imNotEnabled`) when Incident Management is not enabled for the organization
- `403 Forbidden` (`imTeamWriteDenied`) (create only) when your role is not `admin` and you are not a team admin of `teamId`
- `404 Not Found` (`imTeamNotFound`) (create only) when `teamId` does not exist, or belongs to another organization
- `400 Bad Request` (`invalidRequestBody`) (create only) when `name`, `timezone`, or a rotation layer field is missing or malformed (see field descriptions above), or a layer/restriction/user-list bound is exceeded
