Skip to content

Set who is on duty

PUT
/v1/rotations/{rotationId}/on-call
curl --request PUT \
--url https://api.roundrobinbot.eu/v1/rotations/example/on-call \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/*+json' \
--header 'If-Match: example' \
--data '{ "userIds": [ "example" ] }'

Puts the people you name on duty, replacing whoever holds the shift now. They must be members of the rotation, and userIds must name at least one: an empty list is refused rather than read as “take everybody off duty”. Answers with the new on-call state and a fresh ETag. Naming the people who already hold the seats they would take changes nobody’s duty. When they were already put there by name, nothing is written and the same ETag comes back.

rotationId
required
string
If-Match
required
string

The ETag from your last read of this resource, sent back exactly as you received it, quotes included. The write happens only if nothing changed in between: a stale validator is answered 412 and nothing is written. Send * to write against whatever is current on purpose. Omitting the header is 428, never an unconditional write. https://docs.roundrobinbot.eu/api/conventions/#conditional-writes

The body of PUT /v1/rotations/{rotationId}/on-call: who should be on duty.

object
userIds

The Slack user ids to put on duty. At least one, and each must be a member of the rotation.

Array<string>
Examplegenerated
{
"userIds": [
"example"
]
}

OK

Media typeapplication/json

Current on-call assignments for a rotation. Responses with externalAssignments have no ETag; read the assignment freshness and coverage fields on each response.

object
externalAssignments
One of:
null
nextOnCall

Who takes each seat next, one entry per running seat of a native rotation, earliest start first, ties in window order and then lane order. A seat whose window is shut lists the person who comes back when it opens (comingBack); otherwise the person its next hand-over deals. Absences are not resolved, since the scheduler skips somebody who is away only when it deals. Empty for a randomised rotation, which draws at the hand-over, for a switched-off rotation, and for a provider-owned one (read externalAssignments).

Array<object> | null

The person who takes one seat next, and when.

object
cellId

Cell the person takes.

string
comingBack

Whether this is the seat’s holder resuming when a shut window opens, rather than a new person being dealt. Read since as a hand-over only where this is false.

boolean
laneId

Lane containing the cell.

string
laneName

Lane name, when available.

null | string
since

When the person takes the seat in UTC: the window’s next hand-over, or the moment it opens where the same person is coming back. Null where the window has no computed firing (a manual one).

null | string format: date-time
turn

Their position in the native turn order.

integer format: int32
turnEnds

When their turn ends in UTC, where the window has a hand-over after that.

null | string format: date-time
userId

Slack user ID of the person who takes the seat.

string
windowId

Window containing the cell.

string
windowName

Window name, when available.

null | string
nobodyOnCallReason

Why nobody is on call: nobody-on-call-unplanned when a running place is open and has nobody in it, nobody-on-call-as-arranged when every running place is shut by its hours. The two spellings the duty history uses. Absent while any place is live, even if another lane has no one: an empty onCall with no reason here is a person with no Slack identity, so read externalAssignments or unmappedExternalDuty. Places a downgrade stopped count for neither.

null | string
onCall

Current mapped Slack assignments. An empty list does not establish a coverage gap: check externalAssignments or unmappedExternalDuty.

Array<object>

One current mapped Slack assignment. A user can appear more than once when they hold multiple assignments.

object
cellId

Cell holding this assignment.

null | string
coveredUntil

Exclusive end in UTC of this stretch of cover: the window closing, or the turn ending if that comes first. Null while nothing bounds the stretch.

null | string format: date-time
laneId

Lane containing the cell. Lanes for external assignments do not identify provider roles.

null | string
laneName

Lane name, when available.

null | string
since

Assignment start in UTC, when known.

null | string format: date-time
skippedForAbsence

Members skipped for absence when this assignment began.

Array<object>

A member skipped because of an absence at assignment time.

object
from

Absence start in UTC, when known.

null | string format: date-time
source

Absence source: googleCalendar or internalAvailability. Null when the source is unknown.

null | string
until

Absence end in UTC, when known.

null | string format: date-time
userId

Slack user ID of the skipped member.

string
turn

Position in the native turn order. External assignments have no native turn order.

integer format: int32
turnEnds

When this person’s turn ends in UTC: the next hand-over of the seat, which can be later than coveredUntil when the window closes first. Null where nothing hands the seat over.

null | string format: date-time
userId

Slack user ID of the person on duty.

string
windowId

Window containing the cell. External assignment windows do not define provider recurrence.

null | string
windowName

Window name, when available.

null | string
rotationId

Rotation ID.

string
unmappedExternalDuty
One of:
null
users

Directory records for current mapped Slack users and skipped members when ?expand=users is requested. Null otherwise. Expansion disables ETag validation.

Array<object> | null

A Slack user, as Round Robin knows them.

object
avatarUrl

The person’s Slack avatar, at 192px.

null | string
displayName
null | string
email

Null when the install was never granted users:read.email, not only when unknown.

null | string
id

The Slack user id.

string
isDeleted

Deactivated in Slack, or gone from the workspace. These people are still answered rather than hidden, so an id a rotation still names resolves to something you can render.

boolean
isExternal

A Slack Connect person: visible through a shared channel, never a member of the workspace.

boolean
isGuest
boolean
realName
null | string
teamId

The workspace that owns this person’s directory entry, which under Enterprise Grid can differ from the calling key’s workspace.

string
timeZone

The person’s Slack time zone, as an IANA identifier (Europe/Rome). Absent when Round Robin has not seen one for them.

null | string
Examplegenerated
{
"externalAssignments": {
"asOfUtc": "2026-04-15T12:00:00Z",
"assignedAssignmentCount": 1,
"coverage": "example",
"current": [
{
"cellId": "example",
"externalAssignment": {
"memberKind": "example",
"originalEndUtc": "2026-04-15T12:00:00Z",
"originalStartUtc": "2026-04-15T12:00:00Z",
"overrideId": "example",
"participantId": "example",
"rotationId": "example",
"shiftId": "example",
"sourceType": "example"
},
"externalEmail": "example",
"externalName": "example",
"localAssignmentId": "example",
"sinceUtc": "2026-04-15T12:00:00Z",
"untilUtc": "2026-04-15T12:00:00Z",
"user": "example"
}
],
"distinctParticipantCount": 1,
"distinctResponderCount": 1,
"emptyAssignmentCount": 1,
"knownFromUtc": "2026-04-15T12:00:00Z",
"knownUntilUtc": "2026-04-15T12:00:00Z",
"lastReadFailure": "example",
"lastReadFailureUtc": "2026-04-15T12:00:00Z",
"lastSuccessfulReadUtc": "2026-04-15T12:00:00Z",
"nextChange": {
"atUtc": "2026-04-15T12:00:00Z",
"continuing": [
{
"cellId": "example",
"externalAssignment": {
"memberKind": "example",
"originalEndUtc": "2026-04-15T12:00:00Z",
"originalStartUtc": "2026-04-15T12:00:00Z",
"overrideId": "example",
"participantId": "example",
"rotationId": "example",
"shiftId": "example",
"sourceType": "example"
},
"externalEmail": "example",
"externalName": "example",
"localAssignmentId": "example",
"sinceUtc": "2026-04-15T12:00:00Z",
"untilUtc": "2026-04-15T12:00:00Z",
"user": "example"
}
],
"ending": [
{
"cellId": "example",
"externalAssignment": {
"memberKind": "example",
"originalEndUtc": "2026-04-15T12:00:00Z",
"originalStartUtc": "2026-04-15T12:00:00Z",
"overrideId": "example",
"participantId": "example",
"rotationId": "example",
"shiftId": "example",
"sourceType": "example"
},
"externalEmail": "example",
"externalName": "example",
"localAssignmentId": "example",
"sinceUtc": "2026-04-15T12:00:00Z",
"untilUtc": "2026-04-15T12:00:00Z",
"user": "example"
}
],
"starting": [
{
"cellId": "example",
"externalAssignment": {
"memberKind": "example",
"originalEndUtc": "2026-04-15T12:00:00Z",
"originalStartUtc": "2026-04-15T12:00:00Z",
"overrideId": "example",
"participantId": "example",
"rotationId": "example",
"shiftId": "example",
"sourceType": "example"
},
"externalEmail": "example",
"externalName": "example",
"localAssignmentId": "example",
"sinceUtc": "2026-04-15T12:00:00Z",
"untilUtc": "2026-04-15T12:00:00Z",
"user": "example"
}
]
},
"noAnswerReason": "example",
"requiredAssignmentCount": 1,
"state": "example",
"unmappedAssignmentCount": 1
},
"nextOnCall": [
{
"cellId": "example",
"comingBack": true,
"laneId": "example",
"laneName": "example",
"since": "2026-04-15T12:00:00Z",
"turn": 1,
"turnEnds": "2026-04-15T12:00:00Z",
"userId": "example",
"windowId": "example",
"windowName": "example"
}
],
"nobodyOnCallReason": "example",
"onCall": [
{
"cellId": "example",
"coveredUntil": "2026-04-15T12:00:00Z",
"laneId": "example",
"laneName": "example",
"since": "2026-04-15T12:00:00Z",
"skippedForAbsence": [
{
"from": "2026-04-15T12:00:00Z",
"source": "example",
"until": "2026-04-15T12:00:00Z",
"userId": "example"
}
],
"turn": 1,
"turnEnds": "2026-04-15T12:00:00Z",
"userId": "example",
"windowId": "example",
"windowName": "example"
}
],
"rotationId": "example",
"unmappedExternalDuty": {
"email": "example",
"name": "example",
"until": "2026-04-15T12:00:00Z"
},
"users": [
{
"avatarUrl": "example",
"displayName": "example",
"email": "example",
"id": "example",
"isDeleted": true,
"isExternal": true,
"isGuest": true,
"realName": "example",
"teamId": "example",
"timeZone": "example"
}
]
}

Bad Request

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Unauthorized

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Forbidden

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Not Found

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Conflict

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Precondition Failed

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Precondition Required

Media typeapplication/json
object
detail
null | string
instance
null | string
status
null | integer format: int32
title
null | string
type
null | string
Examplegenerated
{
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}