Skip to content

On-call schedule reference

Full request schemas are in the API Reference.

FieldTypeDefaultDescription
namestringrequiredSchedule label, referenced from escalation levels. 1 to 256 characters.
timezonestringUTCIANA zone name from the tz database (America/New_York, Asia/Singapore). Layer handoffs and coverage windows are interpreted in it unless the layer sets its own.

Changing timezone with PUT keeps whoever is on call now on until their next handoff: each layer that follows the schedule zone is re-anchored so the move does not shift the rotation. Layers that have started keep their past in the old zone as an ended version (see Edit a layer), so past shifts do not move either.

Created under /on_call_schedule/{schedule_id}/layer.

FieldTypeDefaultDescription
layer_numberintmax(existing) + 1Layer position, 1 or more, unique in the schedule. Layers with a higher layer_number are evaluated first and shadow lower ones while they cover.
rotation_typeenumrequireddaily, weekly, or custom.
rotation_interval_daysintnoneDays per handoff, 1 to 365. Required for custom; ignored for daily and weekly.
handoff_timestringrequiredHH:MM (24-hour) handoff time in the layer’s zone.
handoff_dayintnoneWeekly layers only: weekday of the handoff, 0=Sunday .. 6=Saturday, in the layer’s zone. Ignored for daily and custom.
participantsuuid[]requiredOrdered list of 1 to 100 users of the account. The rotation walks it and wraps around; a person can appear more than once.
effective_fromrfc3339noneLayer applies only from this instant. The first participant covers from here (or from creation) to the next handoff.
effective_untilrfc3339noneLayer stops applying at this instant. Must be after effective_from.
timezonestringschedule’sIANA zone for this layer’s handoffs and windows, for follow-the-sun layers.
restrictionsobject[][]Coverage windows, at most 28. Empty means the layer covers around the clock.

Responses also carry rotation_anchor, { "date": "2026-10-14", "start_index": 0 }: the local date of one handoff and the participant (0-based) who starts there. The rotation counts whole periods from it, forwards and backwards. Layers created before October 2026 may not have one; their rotation is unaffected.

Rotation periods:

rotation_typePeriod
daily1 day per participant.
weekly7 days per participant.
customrotation_interval_days days per participant.
FieldTypeDescription
typeenumdaily (every day) or weekly (every week).
start_day_of_weekintWeekly only, required: the day the window opens, 0=Sunday .. 6=Saturday.
start_timestringHH:MM (24-hour) local time the window opens.
duration_minutesintHow long it stays open: 1 to 1440 for daily, 1 to 10080 for weekly. A window may run past midnight or past the end of the week.

Example: weeknights from 18:00 to 09:00 plus whole weekends, in a team’s own zone:

{
"rotation_type": "weekly",
"handoff_day": 1,
"handoff_time": "09:00",
"participants": ["<alice>", "<bob>"],
"timezone": "Europe/Berlin",
"restrictions": [
{ "type": "weekly", "start_day_of_week": 1, "start_time": "18:00", "duration_minutes": 900 },
{ "type": "weekly", "start_day_of_week": 2, "start_time": "18:00", "duration_minutes": 900 },
{ "type": "weekly", "start_day_of_week": 3, "start_time": "18:00", "duration_minutes": 900 },
{ "type": "weekly", "start_day_of_week": 4, "start_time": "18:00", "duration_minutes": 900 },
{ "type": "weekly", "start_day_of_week": 5, "start_time": "18:00", "duration_minutes": 3780 }
]
}

PUT /on_call_schedule/{schedule_id}/layer/{layer_id} takes the same body as creating a layer and keeps the layer’s id, so API and Terraform clients can hold on to it. A missing layer_number keeps the current one.

  • A layer that has started keeps its history. The current version ends now and the new one starts now, anchored so whoever the layer has on call stays on until the new rotation’s next handoff. When the period, handoff time, handoff day and zone are unchanged the rotation keeps its handoff dates and only the order moves; otherwise the new rotation counts from its latest handoff. If the person on call now is no longer in participants, the first participant takes over at once. effective_from cannot change, and effective_until must be in the future.
  • A layer that has not started is changed in place, and it never starts in the past.
  • A layer that has ended cannot be edited: add a new layer instead.

The response adds version (1 until the first edit) and version_started_at. Past shifts, the calendar feed and the report keep using each version for the time it covered.

GET /on_call_schedule/{schedule_id}/layer/{layer_id}/versions lists the history: every ended version with its effective_from, effective_until, rotation, zone and anchor, then the current one with "current": true.

Terminal window
curl -X PUT "https://api.siteqwality.com/on_call_schedule/$SCHEDULE_ID/layer/$LAYER_ID" \
-H "Authorization: Bearer $SITEQWALITY_API_KEY" -H "Content-Type: application/json" \
-d '{ "rotation_type": "weekly", "handoff_day": 1, "handoff_time": "09:00",
"participants": ["<carol>", "<alice>", "<bob>"] }'

Created under /on_call_schedule/{schedule_id}/override. An override is a hard “this user is on call” window that beats every layer.

FieldTypeDescription
user_iduuidThe covering user, a user of the account.
start_atrfc3339Window start (inclusive).
end_atrfc3339Window end (exclusive). Must be after start_at.

How “who’s on call at time T” is resolved

Section titled “How “who’s on call at time T” is resolved”
  1. Overrides first. If any override covers T, that user is on call. When overrides overlap, the most recently created one wins.
  2. Layers next, highest layer_number first. A layer is skipped when it has no participants, when T is outside its effective_from / effective_until window (a layer without effective_from starts when it was created), or when it has restrictions and T is in none of them.
  3. For the first layer left, the on-call participant is participants[(start_index + periods) mod n], where periods counts whole rotation periods from the anchor handoff to the latest handoff at or before T. Handoffs fall at handoff_time local on the anchor date plus whole periods, counted on the local calendar.
  4. Handoffs are instants. A handoff whose local time happens twice (the night the clocks go back) happens at the first occurrence; one whose local time is skipped (the night they go forward) happens at the jump.
  5. If nothing matches, the schedule has a coverage gap and resolves to no one.

A new layer is anchored on the latest handoff at or before its effective_from (or its creation), starting with the first participant, so it covers from that instant. A weekly layer with a handoff_day anchors on the latest handoff on that weekday.

All three give the answer the escalation engine uses, so a shift and a point query never disagree.

GET /on_call_schedule/{schedule_id}/on_call

Section titled “GET /on_call_schedule/{schedule_id}/on_call”

Who is on call at at (RFC 3339, default now), the shift’s bounds, and the shift after it.

Terminal window
curl "https://api.siteqwality.com/on_call_schedule/$SCHEDULE_ID/on_call" \
-H "Authorization: Bearer $SITEQWALITY_API_KEY"
{
"data": {
"schedule_id": "4c8395d5-...",
"at": "2026-10-20T15:00:00Z",
"timezone": "America/New_York",
"on_call": {
"user_id": "20400d91-...",
"source": "layer",
"layer_id": "b01a234d-...",
"layer_number": 1,
"starts_at": "2026-10-20T13:00:00Z",
"ends_at": "2026-10-21T13:00:00Z"
},
"next": {
"user_id": "1c887628-...",
"source": "override",
"override_id": "7f0e...",
"starts_at": "2026-10-21T13:00:00Z",
"ends_at": "2026-10-21T18:00:00Z"
}
}
}
  • source is layer, override, or gap. A gap has no user_id.
  • starts_at and ends_at are where the answer changes: a handoff to someone else, a window edge, an override, or a layer starting or ending. A handoff from a person to the same person is not a boundary.
  • A shift that runs more than 90 days past at has no ends_at (or starts_at), and then no next.

GET /on_call_schedule/{schedule_id}/shifts

Section titled “GET /on_call_schedule/{schedule_id}/shifts”

The final timeline: consecutive shifts from since (default now) to until (default 14 days later, at most 90 days after since), overrides applied and gaps included. The first shift can start before since and the last can end after until; each one has the same bounds a point query inside it returns.

Terminal window
curl "https://api.siteqwality.com/on_call_schedule/$SCHEDULE_ID/shifts?since=2026-10-20T00:00:00Z&until=2026-11-03T00:00:00Z" \
-H "Authorization: Bearer $SITEQWALITY_API_KEY"

The response is { "schedule_id", "since", "until", "timezone", "shifts": [...] }, each shift shaped like on_call above.

Shifts across the account’s schedules, earliest first, gaps left out. Each row adds schedule_id and schedule_name.

QueryDescription
user_idOnly this user’s shifts (“my shifts”).
schedule_idOnly this schedule.
sinceRFC 3339, default now.
untilRFC 3339, default since, at most 90 days after it.

With no range it answers “who is on call right now”, one row per covered schedule. GET /on_call?user_id=$ME&until=2026-11-20T00:00:00Z lists your shifts until then.

POST /on_call/feed_tokens creates a calendar feed and returns its address once, in url (and webcal_url for apps that open webcal:// links). Subscribe to it in Google Calendar, Outlook or Apple Calendar; they refresh it about hourly.

Body fieldDescription
user_idWhose feed it is. Defaults to the signed-in user; an API key must name one.
schedule_idOptional. Without it the feed holds the user’s own shifts on every schedule; with it, every shift of that schedule, each titled with the person’s name.

The feed covers 7 days back to 60 days ahead, overrides applied. It shows names only, never an email address or a phone number. The address is the only key, so treat it like a password: GET /on_call/feed_tokens lists feeds without their addresses, and DELETE /on_call/feed_tokens/{feed_id} revokes one at once. A feed also stops when its user leaves the account. Feed requests are rate limited.

@siteqwality/cli 0.1.3 or later reads schedules with an account API key that has read:incidents. It never changes a schedule.

Terminal window
export SITEQWALITY_API_KEY='your-incident-api-key'
npx siteqwality oncall now
npx siteqwality oncall now --schedule "$SCHEDULE_ID" --at 2026-10-21T09:00:00Z
npx siteqwality oncall shifts "$SCHEDULE_ID" --since 2026-10-21T00:00:00Z --until 2026-11-04T00:00:00Z
npx siteqwality oncall mine --user "$MEMBER_ID"

now lists who is on call on every schedule, or one schedule’s current shift and who is next. shifts returns a schedule’s timeline with gaps, 14 days by default and at most 90. mine lists one member’s shifts for the next 14 days; --user is required because an API key does not identify a member.

The MCP server has tools for who is on call, shifts, the report and editing a layer.

In a workspace connected through the Slack integration, /oncall answers privately with who is on call, until when, and who is next. Add part of a schedule name to ask about one schedule, for example /oncall platform.

It answers people it can match to a Site Qwality user. The first time you run it, it may not know you yet. It then replies, only to you, with a Link your Slack account link that works for 15 minutes:

  1. Run /oncall in Slack and click Link your Slack account.

  2. Sign in to Site Qwality if asked. The page shows the Slack workspace and the Slack member being linked.

  3. Click Link Slack account. Site Qwality adds a Slack DM contact method that sends through the workspace’s Slack integration, so no bot token is needed.

  4. Run /oncall again.

If no Site Qwality account has connected the workspace, /oncall says so instead: an admin connects it from Integrations. A workspace connected before October 22, 2026 needs connecting again once: click Add to Slack and pick the same workspace.

You can also add a Slack DM contact method yourself, with a bot token and your Slack member id (it starts with U) as the channel. /oncall finds you either way.

When someone goes on call, Site Qwality can tell them through their low-urgency notification rules (the ones with the shortest delay). A rotation handoff is on_call_starting; a change made by an override starting or ending is on_call_changed. Each notice is sent once, within a few minutes of the handoff, and only to email contact methods: SMS, Slack DM and webhooks get no handoff notice.

GET /reports/on_call shows who carried the on-call load over a range (default the last 30 days, at most 90).

QueryDescription
since, untilRFC 3339 range.
schedule_idOnly this schedule.
user_idOnly this user.

Each entry of users has shifts, on_call_seconds, override_seconds (the part covered by overrides), off_hours_seconds (the part outside 09:00 to 18:00, Monday to Friday, in the schedule’s zone), pages (responder pages accepted for them) and paged_incidents. Each entry of schedules has covered_seconds and gap_seconds.

StatusWhen
400A timestamp that is not RFC 3339 (or outside 1970 to 2200), a malformed UUID, until before since, or a range over 90 days. On writes: an unknown time zone, an empty or overlong name, handoff_time not HH:MM, handoff_day outside 0..6, custom without rotation_interval_days or one outside 1..365, effective_until not after effective_from, no participants or one outside the account, layer_number below 1, a malformed restriction, or an override whose end_at is not after start_at.
404The schedule, or the user_id or schedule_id you filter by, does not exist in your account.
409Creating or editing a layer with a layer_number the schedule already has, or editing a layer that has ended.
MethodPathPurpose
POST / GET/on_call_scheduleCreate / list schedules.
GET / PUT / DELETE/on_call_schedule/{schedule_id}Detail (with layers + overrides) / update / delete.
GET/on_call_schedule/{schedule_id}/on_callWho is on call now (or at at), until when, and who is next.
GET/on_call_schedule/{schedule_id}/shiftsThe final timeline over a range.
GET/on_callWho is on call across schedules, or one user’s shifts.
POST / GET/on_call_schedule/{schedule_id}/layerCreate / list layers.
PUT / DELETE/on_call_schedule/{schedule_id}/layer/{layer_id}Edit a layer, keeping its id / delete a layer.
GET/on_call_schedule/{schedule_id}/layer/{layer_id}/versionsA layer’s history.
POST / GET/on_call_schedule/{schedule_id}/overrideCreate / list overrides.
DELETE/on_call_schedule/{schedule_id}/override/{override_id}Delete an override.
POST / GET/on_call/feed_tokensCreate a calendar feed / list feeds.
DELETE/on_call/feed_tokens/{feed_id}Revoke a calendar feed.
GET/on_call/feed/{token}.icsThe calendar feed itself; no sign-in, the address is the key.
GET/reports/on_callThe on-call load report.

Overrides have no update endpoint: delete and recreate to change one. Deleting a whole schedule needs an admin or an API key. API keys and connected apps scoped to incidents can read the on-call views and the report.