Skip to content

Rotate to the next person on duty

POST
/v1/rotations/{rotationId}/rotate
curl --request POST \
--url https://api.roundrobinbot.eu/v1/rotations/example/rotate \
--header 'Authorization: Bearer <token>' \
--header 'If-Match: example'

Hands the shift on, exactly as the Rotate button does. Answers with the new on-call state and a fresh ETag.

rotationId
required
string
If-Match
required
string

The ETag from your last read of this rotation, 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

OK

Media typeapplication/json

Who is on call for a rotation right now.

object
rotationId
string
onCall

Empty when nobody holds the shift, and also when an external rotation’s on-duty person has no Slack identity: read unmappedExternalDuty before concluding “nobody”.

Array<object>

One person holding the shift, and when they hold it until.

object
userId

The Slack user id of the person actually on duty.

string
turn

Their position in the rotation’s turn order.

integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
since
null | string format: date-time
until
null | string format: date-time
skippedForAbsence

Members the plan would otherwise have named, who were passed over because they were absent.

Array<object>

A member the rotation passed over because they were away.

object
userId

The Slack user id of the member who was passed over.

string
from
null | string format: date-time
until
null | string format: date-time
source

Where the absence came from: googleCalendar or internalAvailability. Null on older records, which predate the field, so treat it as optional.

null | string
unmappedExternalDuty
One of:
null
users

Every Slack user this body names, resolved, when the caller asked for ?expand=users. Null otherwise, so a client can tell “not asked for” from “asked for and nobody matched”.

Array<object> | null

A Slack user, as Round Robin knows them.

object
id

The Slack user id.

string
teamId

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

string
displayName
null | string
realName
null | string
email

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

null | string
isGuest
boolean
isExternal

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

boolean
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
avatarUrl

The person’s Slack avatar, at 192px.

null | 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
{
"rotationId": "example",
"onCall": [
{
"userId": "example",
"turn": 1,
"since": "2026-04-15T12:00:00Z",
"until": "2026-04-15T12:00:00Z",
"skippedForAbsence": [
{
"userId": "example",
"from": "2026-04-15T12:00:00Z",
"until": "2026-04-15T12:00:00Z",
"source": "example"
}
]
}
],
"unmappedExternalDuty": {
"email": "example",
"name": "example",
"until": "2026-04-15T12:00:00Z"
},
"users": [
{
"id": "example",
"teamId": "example",
"displayName": "example",
"realName": "example",
"email": "example",
"isGuest": true,
"isExternal": true,
"isDeleted": true,
"avatarUrl": "example",
"timeZone": "example"
}
]
}

Bad Request

Media typeapplication/json
object
type
null | string
title
null | string
status
null | integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
detail
null | string
instance
null | string
Examplegenerated
{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"instance": "example"
}

Unauthorized

Media typeapplication/json
object
type
null | string
title
null | string
status
null | integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
detail
null | string
instance
null | string
Examplegenerated
{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"instance": "example"
}

Forbidden

Media typeapplication/json
object
type
null | string
title
null | string
status
null | integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
detail
null | string
instance
null | string
Examplegenerated
{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"instance": "example"
}

Not Found

Media typeapplication/json
object
type
null | string
title
null | string
status
null | integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
detail
null | string
instance
null | string
Examplegenerated
{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"instance": "example"
}

Precondition Failed

Media typeapplication/json
object
type
null | string
title
null | string
status
null | integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
detail
null | string
instance
null | string
Examplegenerated
{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"instance": "example"
}

Precondition Required

Media typeapplication/json
object
type
null | string
title
null | string
status
null | integer | string format: int32
/^-?(?:0|[1-9]\d*)$/
detail
null | string
instance
null | string
Examplegenerated
{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"instance": "example"
}