The Round Robin API
The public API reads and changes on-call duty under /v1. Use it to connect an integration to Round Robin, or to submit a decision for a signed-in Slack member.
You need: the credential the operation asks for. Most operations take a team API key. Participant actions take a Round Robin access token for a signed-in Slack member.
Make your first call
Section titled “Make your first call”Create an integration key under Settings → API keys. Call GET /v1/keys/self with the key in the bearer header to check its workspace, scopes and rotation access.
The production API is https://api.roundrobinbot.eu. Use credentials issued for that environment. See authentication.
Find a rotation
Section titled “Find a rotation”GET /v1/rotations lists the rotations the key can reach, a page at a time. Narrow it with q (part of the name), enabled or mode.
To find the rotation behind a short code such as PAYMENTS, pass code. It matches the whole code, in any case, so at most one rotation comes back:
GET /v1/rotations?code=paymentsAn empty data means no rotation the key can reach has that code. A code that is empty or blank is ignored, and the whole list comes back.
Ask who is on call
Section titled “Ask who is on call”Call GET /v1/rotations/{rotationId}/on-call. Each entry in onCall is one person on duty, with the lane and window they hold, when their cover stops and when their turn ends. When onCall is empty, nobodyOnCallReason says why. nextOnCall says who takes each place next. Add expand=users for the directory details of each person named.
GET /v1/rotations/{rotationId} carries the same answer under onCall, so a rotation read needs no second call.
When a response returns an ETag, keep it for supported conditional reads or writes. See conventions.
When duty ends
Section titled “When duty ends”An entry carries two end times, because they answer two questions.
| Field | What it says | Null when |
|---|---|---|
coveredUntil |
When this person stops covering. That is the close of the window they are in, or the end of their turn if it comes first. An absence or a direct assignment can bring it earlier. Read it to say until when somebody can be reached. | The window is open around the clock and the turn has no end. On a cell holder, also while they are not covering. |
turnEnds |
When their turn ends: the next hand-over, when the seat passes to somebody else. A window closing does not move it. Read it for a reminder before a shift ends, or to say when the next person takes over. | Nothing hands the seat over, as in a window with the Manual schedule type. |
Both are UTC, and both are exclusive: at that instant the cover, or the turn, has already ended.
The two differ when the window closes before the hand-over. Take a window open 09:00 to 17:00 with a hand-over at 21:00. During the day, the entry reads:
{ "userId": "U024BE7LH", "coveredUntil": "2026-10-05T17:00:00+00:00", "turnEnds": "2026-10-05T21:00:00+00:00"}At 17:00 the window closes and the person leaves onCall. Nobody in that window is on call until it opens again. If every other window is shut as well, nobodyOnCallReason reads nobody-on-call-as-arranged.
When nobody is on call
Section titled “When nobody is on call”Read nobodyOnCallReason to tell a quiet hour from a gap that needs a person.
| Value | What it means | What to do |
|---|---|---|
null |
Somebody is on call. | Nothing. |
nobody-on-call-unplanned |
A window is open and nobody holds it. | Put somebody on duty in that window. |
nobody-on-call-as-arranged |
Every window is shut, as on nights and weekends. | Nothing. Cover comes back when a window opens: each window’s nextCoverage.startUtc says when. |
null means at least one person is on call somewhere in the rotation. It stays null when one lane has nobody while another lane is covered, so it does not report a single empty lane: read cells for that.
An empty onCall with a null reason is a partner responder with no Slack account. Read externalAssignments or unmappedExternalDuty to see who it is.
When the partner schedule has no answer
Section titled “When the partner schedule has no answer”A rotation linked to PagerDuty or Jira Service Management always carries externalAssignments. When Round Robin has no usable answer from the provider, externalAssignments.noAnswerReason says why. Nobody is on call in the rotation then: onCall and externalAssignments.current are empty.
| Value | What it means | What to do |
|---|---|---|
null |
The provider’s schedule answers. | Read externalAssignments.current. |
switchedOff |
The rotation is switched off (enabled is false). |
Enable the rotation. |
bindingMissing |
The rotation has no link to a provider schedule. | Link the schedule again in the dashboard. |
notReadYet |
Round Robin has not read the schedule since it was linked. | Ask again in a minute. If it stays, check the provider connection in the dashboard. |
snapshotNotCurrent |
The last read of the schedule does not cover asOfUtc. |
Check the provider connection in the dashboard, and that the schedule still exists at the provider. |
nobodyOnCallReason reads nobody-on-call-as-arranged for switchedOff, and nobody-on-call-unplanned for the other three. The dashboard and Slack show the same four as Rotation disabled, Schedule not linked, Schedule not read yet and Schedule data out of date: see When the provider has no answer.
A stopped cell counts for neither value.
The same two values are the reason on the nobody.on_call webhook and on a gap in a rotation’s history.
Who is next
Section titled “Who is next”nextOnCall has one entry per running place: the person who takes it next. Entries are ordered by since, earliest first. A tie goes in window order, then lane order.
Each entry names the place with cellId, laneId, laneName, windowId and windowName, like an onCall entry, and the person with userId and turn.
| Field | What it says | Null when |
|---|---|---|
since |
When the person takes the place, in UTC: the window’s next hand-over, or the moment a shut window opens where the same person is coming back. | The window has the Manual schedule type. |
turnEnds |
When their turn ends, in UTC. | The window has no hand-over after that. |
comingBack |
true when this is the place’s holder resuming as a shut window opens, false when a new person is dealt. Read since as a hand-over only where this is false. |
Never. |
Absences are not taken into account. Somebody who is away at the hand-over is skipped then, so the person who takes over can differ from the entry.
nextOnCall is empty when:
- the rotation has Random order on, because the person is drawn at the hand-over;
- the rotation is switched off (
enabledisfalse); - the rotation follows a partner schedule. Read
externalAssignments.nextChangeinstead.
Webhook bodies leave nextOnCall out. Call this endpoint when you need it.
What you can read
Section titled “What you can read”| Task | Endpoint |
|---|---|
| Find rotations | GET /v1/rotations |
| Read a rotation | GET /v1/rotations/{rotationId} |
| Read projected duty | GET /v1/rotations/{rotationId}/schedule |
| Read one person’s shifts | GET /v1/users/{userId}/shifts |
| Read direct assignments | GET /v1/rotations/{rotationId}/duty-arrangements |
| Read cover requests | GET /v1/rotations/{rotationId}/cover-requests |
| Read swap requests | GET /v1/rotations/{rotationId}/swap-requests |
| Find duty for a swap | GET /v1/rotations/{rotationId}/swap-requests/candidates |
Supply the range and pagination parameters each operation requires. Read every page before concluding that somebody has no duty.
What you can change
Section titled “What you can change”| Task | Access |
|---|---|
| Rotate or set on duty | Team API key with write:duty |
| Create, change or restore a direct assignment | Team API key with write:duty |
| Create or configure rotations and coverage | Team API key with write:rotation |
| Manage rotation availability | Team API key with write:rotation |
| Propose or respond to cover and swaps | Authenticated member with the required participant permission |
Read or preview the current state before submitting. Most writes to an existing rotation require If-Match; duty actions also use an operation identifier for retries. See conventions and participant duty operations.
For direct assignments, use the duty-arrangements/preview operation before creating a replacement. Read {arrangementId}/restore-preview before ending one. Ending it hands the duty back by the current schedule and availability, so the original person does not always return. Each arrangement’s phase and changesAtUtc, and each cell’s actionable, are described in Read where a request stands.
Read the past
Section titled “Read the past”GET /v1/rotations/{rotationId}/history is the record of who actually held duty. For duty still to come, read schedule.
A row with no user is not always a gap. A gap nobody covered carries reason: nobody-on-call-unplanned or nobody-on-call-as-arranged, with the same meaning as on the on-call response.
Read one person’s shifts
Section titled “Read one person’s shifts”GET /v1/users/{userId}/shifts lists a Slack member’s duty across the rotations the key can read, recorded and projected together. It needs read:oncall. Request a range of at most 31 days, and read every page.
Each row is one interval. source says which kind it is: history for recorded duty, projected for duty to come. Each rotation in rotations has its own asOfUtc, the instant that separates the two.
A turn on a window with hours is several rows, one per opening. Two fields join them back into turns:
| Field | What it says |
|---|---|
turn |
The person’s position in the list the cell deals from. It is a position, not a counter: in a three-person weekly rotation, turn 0 comes round again three weeks later. |
onDutySinceIsDutyShift |
true when the row begins at the start of the person’s turn. The rows after it begin because cover opened. It is false on a row that begins at the rotation’s asOfUtc, so a turn already under way claims no start. |
To rebuild turns, group rows by cellId, userId and turn, and start a new group at each row where onDutySinceIsDutyShift is true.
Each row also says where it stands at its rotation’s asOfUtc, so you never compare its times with your own clock:
| Field | What it says |
|---|---|
phase |
upcoming, running or ended. A row with no end never ends. A history row is always ended. |
changesAtUtc |
When phase next changes: the start while upcoming, the end while running. Absent when it will not change. |
Check each rotation’s completion and limitationCodes before concluding a person has no duty. The codes are listed in conventions.
Read who changed what
Section titled “Read who changed what”GET /v1/rotations/{rotationId}/activity lists recorded changes to a rotation and who made each one. Narrow it with its date range.
Lanes, windows and cells
Section titled “Lanes, windows and cells”A lane is a place for one person on duty. A coverage window sets its hours and time zone. A cell pairs a lane with a window, and GET /v1/rotations/{rotationId} lists every pair in cells.
A cell somebody holds carries a holder, with the same coveredUntil and turnEnds as an on-call entry. The holder stays on the cell while its window is shut, with waiting set to true and coveredUntil null. They are not on call then. They come back when the window opens, unless their turn ends first, and the window’s nextCoverage.startUtc says when that is. So read onCall to know who is on duty now, and cells to know who holds each place.
A cell with stopped set to true is defined but not running, because the workspace is not on a paid plan. It holds nobody.
Each cell deals from one of the rotation’s lists. rosters holds every list, with its rosterId, its name and its turn order, and each cell carries the rosterId it deals from. name is null when the list could not be read.
A rotation that follows a provider
Section titled “A rotation that follows a provider”A rotation linked to a PagerDuty or Jira Service Management schedule takes its hours from the provider, and publishes none of its own:
- each window has an empty
coverage,alwaysOpenset tofalse, and a nullname; - each lane has a null
name; onCallentries carry nolaneNameorwindowName.
Read externalAssignments for who the provider has assigned and when that changes. See external rotations.
Change the grid
Section titled “Change the grid”PUT /v1/rotations/{rotationId}/coverage replaces the rotation’s lanes, windows and cells with the ones you send. It removes any lane or window the body leaves out, along with whoever held it. So read the rotation first, make your change to what it returned, and send the whole grid back with its ETag in If-Match.
Every lane and window needs an id. For one you are adding, use any string of your own, and use the same string in its cells.
To add a window, send its cells, one for each lane. Or send the window with a rosterId and no cells, and a cell is written in every lane, each dealing from that list. In windows:
{ "id": "tokyo", "timeZone": "Asia/Tokyo", "rosterId": "68b0c3e1f2a4d5b6c7e8f901" }rosterId works only on a window you are adding. Any cells you do send for that window are kept as you sent them.
Every rosterId, on a cell or a window, must be one of this rotation’s own lists. A list another cell of the rotation already deals from is accepted too.
On an enabled rotation, a place the replacement leaves empty is dealt at once, so a lane you add has somebody on it when the response comes back. A switched-off rotation (enabled is false) accepts the same replacement and puts nobody on duty: every cell stays empty until the rotation is switched on, and then fills the way switching it back on describes.
| Refusal | title |
What to do |
|---|---|---|
400 invalid_request |
The grid is not dense |
Send one cell for every lane and window pair, or a rosterId on each window you add. The detail gives the count expected and the count sent. |
400 invalid_request |
A window the rotation already has takes no rosterId |
Remove rosterId from that window. It keeps the cells it has. |
400 invalid_request |
A cell names a list that is not this rotation's |
Use a rosterId from the rotation’s rosters or from its cells. |
400 invalid_request |
A window's hours are not valid or A window's cadence is out of range |
Correct the window the detail names. |
402 payment_required |
Send a smaller grid, or check the workspace plan. A workspace that changed plan keeps editing the grid it already has. | |
409 external_rotation_is_read_only |
The rotation follows a PagerDuty or Jira Service Management schedule. Change the schedule at the provider. | |
412 precondition_failed |
The rotation changed since you read it. Read it again and send its new ETag in If-Match. |
Say who is away
Section titled “Say who is away”POST /v1/rotations/{rotationId}/availability records an absence for a member. Check the person, the dates and appliesTo before you send it.
To see what a proposed absence would change first, use the availability impact preview. It saves nothing. Direct assignments, accepted cover and agreed swaps stay in place after an absence: review them separately.
Versions
Section titled “Versions”Build only on the /v1 routes in the API reference. Other routes serve the dashboard and are not part of the public API.
Next steps
Section titled “Next steps”- Authentication: select and send a credential.
- Scopes and access: restrict an integration key.
- Participant duty operations: preview and submit member decisions.
- API reference: operation parameters and response schemas.
