On-call schedule reference
Full request schemas are in the API Reference.
Schedule
Section titled “Schedule”| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Schedule label, referenced from escalation levels. 1 to 256 characters. |
timezone | string | UTC | IANA zone name from the tz database (America/, Asia/). 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.
| Field | Type | Default | Description |
|---|---|---|---|
layer_number | int | max(existing) + 1 | Layer 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_type | enum | required | daily, weekly, or custom. |
rotation_interval_days | int | none | Days per handoff, 1 to 365. Required for custom; ignored for daily and weekly. |
handoff_time | string | required | HH:MM (24-hour) handoff time in the layer’s zone. |
handoff_day | int | none | Weekly layers only: weekday of the handoff, 0=Sunday .. 6=Saturday, in the layer’s zone. Ignored for daily and custom. |
participants | uuid[] | required | Ordered list of 1 to 100 users of the account. The rotation walks it and wraps around; a person can appear more than once. |
effective_from | rfc3339 | none | Layer applies only from this instant. The first participant covers from here (or from creation) to the next handoff. |
effective_until | rfc3339 | none | Layer stops applying at this instant. Must be after effective_from. |
timezone | string | schedule’s | IANA zone for this layer’s handoffs and windows, for follow-the-sun layers. |
restrictions | object[] | [] | 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_type | Period |
|---|---|
daily | 1 day per participant. |
weekly | 7 days per participant. |
custom | rotation_interval_days days per participant. |
Restriction
Section titled “Restriction”| Field | Type | Description |
|---|---|---|
type | enum | daily (every day) or weekly (every week). |
start_day_of_week | int | Weekly only, required: the day the window opens, 0=Sunday .. 6=Saturday. |
start_time | string | HH:MM (24-hour) local time the window opens. |
duration_minutes | int | How 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 } ]}Edit a layer
Section titled “Edit a layer”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_fromcannot change, andeffective_untilmust 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.
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>"] }'Override
Section titled “Override”Created under /on_call_schedule/{schedule_id}/override. An override is a hard “this user is on call” window that beats every layer.
| Field | Type | Description |
|---|---|---|
user_id | uuid | The covering user, a user of the account. |
start_at | rfc3339 | Window start (inclusive). |
end_at | rfc3339 | Window 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”- Overrides first. If any override covers
T, that user is on call. When overrides overlap, the most recently created one wins. - Layers next, highest
layer_numberfirst. A layer is skipped when it has no participants, whenTis outside itseffective_from/effective_untilwindow (a layer withouteffective_fromstarts when it was created), or when it has restrictions andTis in none of them. - For the first layer left, the on-call participant is
participants[(start_index + periods) mod n], whereperiodscounts whole rotation periods from the anchor handoff to the latest handoff at or beforeT. Handoffs fall athandoff_timelocal on the anchor date plus whole periods, counted on the local calendar. - 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.
- 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.
See who’s on call
Section titled “See who’s on call”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.
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" } }}sourceislayer,override, orgap. A gap has nouser_id.starts_atandends_atare 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
athas noends_at(orstarts_at), and then nonext.
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.
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.
GET /on_call
Section titled “GET /on_call”Shifts across the account’s schedules, earliest first, gaps left out. Each row adds schedule_id and schedule_name.
| Query | Description |
|---|---|
user_id | Only this user’s shifts (“my shifts”). |
schedule_id | Only this schedule. |
since | RFC 3339, default now. |
until | RFC 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.
In a calendar
Section titled “In a calendar”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 field | Description |
|---|---|
user_id | Whose feed it is. Defaults to the signed-in user; an API key must name one. |
schedule_id | Optional. 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.
From the CLI
Section titled “From the CLI”@siteqwality/cli 0.1.3 or later reads schedules with an account API key that has read:incidents. It never changes a schedule.
export SITEQWALITY_API_KEY='your-incident-api-key'npx siteqwality oncall nownpx siteqwality oncall now --schedule "$SCHEDULE_ID" --at 2026-10-21T09:00:00Znpx siteqwality oncall shifts "$SCHEDULE_ID" --since 2026-10-21T00:00:00Z --until 2026-11-04T00:00:00Znpx 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.
From an assistant
Section titled “From an assistant”The MCP server has tools for who is on call, shifts, the report and editing a layer.
From Slack
Section titled “From Slack”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:
-
Run
/oncallin Slack and click Link your Slack account. -
Sign in to Site Qwality if asked. The page shows the Slack workspace and the Slack member being linked.
-
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.
-
Run
/oncallagain.
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.
Handoff notices
Section titled “Handoff notices”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.
On-call report
Section titled “On-call report”GET /reports/on_call shows who carried the on-call load over a range (default the last 30 days, at most 90).
| Query | Description |
|---|---|
since, until | RFC 3339 range. |
schedule_id | Only this schedule. |
user_id | Only 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.
Errors
Section titled “Errors”| Status | When |
|---|---|
400 | A 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. |
404 | The schedule, or the user_id or schedule_id you filter by, does not exist in your account. |
409 | Creating or editing a layer with a layer_number the schedule already has, or editing a layer that has ended. |
Endpoints
Section titled “Endpoints”| Method | Path | Purpose |
|---|---|---|
POST / GET | /on_call_schedule | Create / list schedules. |
GET / PUT / DELETE | /on_call_schedule/ | Detail (with layers + overrides) / update / delete. |
GET | /on_call_schedule/ | Who is on call now (or at at), until when, and who is next. |
GET | /on_call_schedule/ | The final timeline over a range. |
GET | /on_call | Who is on call across schedules, or one user’s shifts. |
POST / GET | /on_call_schedule/ | Create / list layers. |
PUT / DELETE | /on_call_schedule/ | Edit a layer, keeping its id / delete a layer. |
GET | /on_call_schedule/ | A layer’s history. |
POST / GET | /on_call_schedule/ | Create / list overrides. |
DELETE | /on_call_schedule/ | Delete an override. |
POST / GET | /on_call/ | Create a calendar feed / list feeds. |
DELETE | /on_call/ | Revoke a calendar feed. |
GET | /on_call/ | The calendar feed itself; no sign-in, the address is the key. |
GET | /reports/ | The 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.
See also
Section titled “See also”- On-call overview for patterns (weekly rotation, business hours, weekends, follow the sun, primary + secondary).
- Rotation overrides for holiday cover and swaps.
- Escalation policy reference for wiring schedules into paging.