Update a schedule
Change an on-call schedule's name, timezone, validity range, weekly windows, rotation cadence and rotation membership — in one atomic call.
PATCH /api/im/schedules/:id
This is where a schedule's rotation actually gets configured. Creating a schedule gives you an empty shell (rotationMode: "none", no members); this endpoint sets the cadence, the weekly windows and who is in the rotation.
Every field is optional. Only the fields present in the body are touched — a request body of { "name": "Primary" } renames the schedule and changes nothing else.
Authentication
Requires the base IM access every endpoint in this API needs, plus 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 is treated as an organization admin.
Incident Management is currently restricted to platform administrators. While that restriction is in place, organization roles and organization-wide API tokens receive 403 imAccessDenied on every /api/im/** endpoint, including this one.
Request body
| Field | Type | Description |
|---|---|---|
name | string | Schedule name, up to 120 characters. |
timezone | string | IANA timezone (e.g. Europe/Berlin). Every handover boundary and weekly window is evaluated in this zone, across DST. |
effectiveFrom | string | null | ISO timestamp. The schedule pages nobody before it. null = no start bound. |
effectiveUntil | string | null | ISO timestamp. The schedule pages nobody after it. null = no end bound. |
weeklySchedules | array | Recurring on-call windows, up to 50. Each: { from: "HH:mm", until: "HH:mm", days: number[] }, days as ISO weekdays (1 = Monday .. 7 = Sunday). until may be "24:00" for end-of-day, and an until at or before from wraps past midnight. An empty array means 24/7. |
rotations | array | Full replacement of the rotation groups, up to 50. Each: { members: string[] }, up to 200 members. Array order is the rotation order; member order within a group is preserved. Every member must already be a member of the schedule's team. Omit the field to leave rotations untouched — sending [] removes them all. |
Plus the cadence fields — rotationMode, rotationRepeats, customRepeatUnit, customRepeatValue, autoRotationSize, roundRobinSize, startsOnTime, startsOnDayOfWeek, startsOnDateOfMonth. They are documented once, with their allowed values, under Rotation cadence.
teamId and tierId are not patchable. Moving a schedule between teams or tiers would silently re-point who it pages, so it is not a partial update.
How the cadence is validated
Cadence fields are merged onto the schedule's current row and then validated as a whole, not field by field. That is what lets you patch a single field — say roundRobinSize — without re-sending the rest of the cadence, while still rejecting a combination that cannot work (for example rotationRepeats: "weekly" with no startsOnDayOfWeek anywhere in the effective config).
Validation happens before anything is written. A cadence the materializer would choke on never reaches the database.
When shifts are recomputed
Changing the timezone, the validity range, the weekly windows, any cadence field, or the rotations re-materializes the schedule's shifts. A bare rename does not — it would take the schedule's lock and rewrite rows for nothing.
Example (cURL)
curl -X PATCH "$BASE_URL/api/im/schedules/7" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rotationMode": "explicit",
"rotationRepeats": "custom",
"customRepeatUnit": "minutes",
"customRepeatValue": 30,
"startsOnTime": "09:00",
"rotations": [
{ "members": ["u_abc123"] },
{ "members": ["u_def456"] }
]
}'Response
200 OK: the schedule's configuration after the update.
{
"id": 7,
"displayName": "Primary On-Call",
"timezone": "Europe/Berlin",
"effectiveFrom": "2026-08-01T00:00:00.000Z",
"effectiveUntil": null,
"weeklySchedules": [],
"rotationMode": "explicit",
"rotationRepeats": "custom",
"customRepeatUnit": "minutes",
"customRepeatValue": 30,
"autoRotationSize": null,
"roundRobinSize": 1,
"startsOnDayOfWeek": null,
"startsOnDateOfMonth": null,
"startsOnTime": "09:00",
"rotations": [
{
"id": 12,
"rotationOrder": 1,
"members": [{ "userId": "u_abc123", "userName": "Ada Lovelace", "memberOrder": 1 }]
},
{
"id": 13,
"rotationOrder": 2,
"members": [{ "userId": "u_def456", "userName": "Bob Fixit", "memberOrder": 1 }]
}
]
}The rotations key is present only when you sent rotations. A request that did not touch them omits the key entirely rather than re-reading state you did not change; use GET /api/im/teams/:id/escalations if you need the current rotation set.
Common errors
| Status | data.code | Cause |
|---|---|---|
| 400 | invalidRequestBody | Non-numeric :id, a missing or over-long name, an invalid timezone, or a malformed rotations entry (not an object, members not an array, an empty user id). |
| 403 | imAccessDenied | No Incident Management access — see the note under Authentication. |
| 403 | imTeamWriteDenied | IM access, but neither organization admin nor team admin of this schedule's team. |
| 404 | imScheduleNotFound | No such schedule in your organization. |
| 404 | imTeamMemberNotFound | A user id in rotations is not a member of the schedule's team. |
| 422 | imInvalidRotationMode | rotationMode is not none, auto or explicit. |
| 422 | imInvalidRotation | The cadence does not hold together — an unknown rotationRepeats or customRepeatUnit, a startsOnDateOfMonth outside 1–31, a required field missing for the effective cadence, too many rotations or members, or a duplicate user id inside one group. |
| 422 | imInvalidNumber | customRepeatValue, autoRotationSize or roundRobinSize is not an integer ≥ 1. |
| 422 | imInvalidTimeOfDay | startsOnTime or a weekly window's from/until is not "HH:mm". |
| 422 | imInvalidDayOfWeek | startsOnDayOfWeek, or a day inside weeklySchedules, is not an ISO weekday 1–7. |
| 422 | imInvalidTimeFilter | weeklySchedules is not an array, has more than 50 entries, or an entry is not an object. |