Create a rotation
using System.Net.Http.Headers;var client = new HttpClient();var request = new HttpRequestMessage{ Method = HttpMethod.Post, RequestUri = new Uri("https://api.roundrobinbot.eu/v1/rotations"), Headers = { { "Idempotency-Key", "example" }, { "Authorization", "Bearer <token>" }, }, Content = new StringContent("{ \"name\": \"example\", \"description\": \"example\", \"code\": \"example\", \"public\": true, \"dutySize\": 1, \"owners\": [ \"example\" ], \"channels\": [ \"example\" ], \"userIds\": [ \"example\" ], \"userGroupIds\": [ \"example\" ] }") { Headers = { ContentType = new MediaTypeHeaderValue("application/json-patch+json") } }};using (var response = await client.SendAsync(request)){ response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); Console.WriteLine(body);}package main
import ( "fmt" "strings" "net/http" "io")
func main() {
url := "https://api.roundrobinbot.eu/v1/rotations"
payload := strings.NewReader("{ \"name\": \"example\", \"description\": \"example\", \"code\": \"example\", \"public\": true, \"dutySize\": 1, \"owners\": [ \"example\" ], \"channels\": [ \"example\" ], \"userIds\": [ \"example\" ], \"userGroupIds\": [ \"example\" ] }")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "example") req.Header.Add("Authorization", "Bearer <token>") req.Header.Add("Content-Type", "application/json-patch+json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(res) fmt.Println(string(body))
}OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json-patch+json");RequestBody body = RequestBody.create(mediaType, "{ \"name\": \"example\", \"description\": \"example\", \"code\": \"example\", \"public\": true, \"dutySize\": 1, \"owners\": [ \"example\" ], \"channels\": [ \"example\" ], \"userIds\": [ \"example\" ], \"userGroupIds\": [ \"example\" ] }");Request request = new Request.Builder() .url("https://api.roundrobinbot.eu/v1/rotations") .post(body) .addHeader("Idempotency-Key", "example") .addHeader("Authorization", "Bearer <token>") .addHeader("Content-Type", "application/json-patch+json") .build();
Response response = client.newCall(request).execute();import axios from 'axios';
const options = { method: 'POST', url: 'https://api.roundrobinbot.eu/v1/rotations', headers: { 'Idempotency-Key': 'example', Authorization: 'Bearer <token>', 'Content-Type': 'application/json-patch+json' }, data: '{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ] }'};
try { const { data } = await axios.request(options); console.log(data);} catch (error) { console.error(error);}const url = 'https://api.roundrobinbot.eu/v1/rotations';const options = { method: 'POST', headers: { 'Idempotency-Key': 'example', Authorization: 'Bearer <token>', 'Content-Type': 'application/json-patch+json' }, body: '{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ] }'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.roundrobinbot.eu/v1/rotations \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json-patch+json' \ --header 'Idempotency-Key: example' \ --data '{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ] }'Answers 201 with the new rotation, its Location and its ETag. name and at least one entry in owners are required. The rotation starts enabled and manual: PUT /v1/rotations/{rotationId}/schedule is what puts it on automatic. A retry carrying the same Idempotency-Key answers 200 with the rotation the first attempt made, having created nothing. A key restricted to particular rotations cannot create.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”A value you generate for this create attempt and reuse on every retry of it, so a retried request cannot make a second rotation. A UUID is the usual choice. Repeating the key within 24 hours answers 200 with the rotation it already created; after that the key is spent and the create is refused 409. Omitting the header is 428. https://docs.roundrobinbot.eu/api/conventions/#creating-things
Request Bodyrequired
Section titled “Request Bodyrequired”The rotation POST /v1/rotations creates.
object
What the rotation is called. Required, and not unique: two rotations may share a name.
A sentence about what the rotation covers.
The short code the rotation answers to in Slack. Must be unique in the workspace, and a clash is refused rather than resolved.
Whether every member of the workspace can see the rotation. Defaults to false, which leaves it visible to its members, its owners and workspace admins.
How many people hold the shift at once. Defaults to 1, and more than 1 is a paid-plan feature.
The Slack user ids that may administer the rotation. Required, and at least one.
The Slack channel ids the rotation posts to. The app must already be in every channel named.
The Slack user ids in the rotation, in the order duty passes through them.
Slack user group ids whose members are in the rotation. The rotation follows the group: add or remove somebody in Slack and the turn order changes with it.
Examplegenerated
{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ]}The rotation POST /v1/rotations creates.
object
What the rotation is called. Required, and not unique: two rotations may share a name.
A sentence about what the rotation covers.
The short code the rotation answers to in Slack. Must be unique in the workspace, and a clash is refused rather than resolved.
Whether every member of the workspace can see the rotation. Defaults to false, which leaves it visible to its members, its owners and workspace admins.
How many people hold the shift at once. Defaults to 1, and more than 1 is a paid-plan feature.
The Slack user ids that may administer the rotation. Required, and at least one.
The Slack channel ids the rotation posts to. The app must already be in every channel named.
The Slack user ids in the rotation, in the order duty passes through them.
Slack user group ids whose members are in the rotation. The rotation follows the group: add or remove somebody in Slack and the turn order changes with it.
Examplegenerated
{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ]}The rotation POST /v1/rotations creates.
object
What the rotation is called. Required, and not unique: two rotations may share a name.
A sentence about what the rotation covers.
The short code the rotation answers to in Slack. Must be unique in the workspace, and a clash is refused rather than resolved.
Whether every member of the workspace can see the rotation. Defaults to false, which leaves it visible to its members, its owners and workspace admins.
How many people hold the shift at once. Defaults to 1, and more than 1 is a paid-plan feature.
The Slack user ids that may administer the rotation. Required, and at least one.
The Slack channel ids the rotation posts to. The app must already be in every channel named.
The Slack user ids in the rotation, in the order duty passes through them.
Slack user group ids whose members are in the rotation. The rotation follows the group: add or remove somebody in Slack and the turn order changes with it.
Examplegenerated
{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ]}The rotation POST /v1/rotations creates.
object
What the rotation is called. Required, and not unique: two rotations may share a name.
A sentence about what the rotation covers.
The short code the rotation answers to in Slack. Must be unique in the workspace, and a clash is refused rather than resolved.
Whether every member of the workspace can see the rotation. Defaults to false, which leaves it visible to its members, its owners and workspace admins.
How many people hold the shift at once. Defaults to 1, and more than 1 is a paid-plan feature.
The Slack user ids that may administer the rotation. Required, and at least one.
The Slack channel ids the rotation posts to. The app must already be in every channel named.
The Slack user ids in the rotation, in the order duty passes through them.
Slack user group ids whose members are in the rotation. The rotation follows the group: add or remove somebody in Slack and the turn order changes with it.
Examplegenerated
{ "name": "example", "description": "example", "code": "example", "public": true, "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "userIds": [ "example" ], "userGroupIds": [ "example" ]}Responses
Section titled “Responses”OK
A rotation: who is in it, when it hands over, and who holds the shift right now.
object
The short code the rotation answers to in Slack.
Whether every member of the workspace can see the rotation.
manual, auto or external.
How many people hold the shift at once.
Everybody in the rotation, split by how they got there.
object
Members added individually.
One member of a rotation.
object
Where the member sits in the order the plan is built from.
Members that came in through a Slack user group, listed under the group they came from. Add or remove somebody in Slack’s group and the rotation follows.
A Slack user group in the rotation, and the people currently in it.
object
One member of a rotation.
object
Where the member sits in the order the plan is built from.
The turn order.
object
One position in the turn order.
object
Set when the member is in the plan by way of a user group.
When the rotation hands over. Read type first: it says which of the fields below carry anything.
object
One of dayOfWeek, daysOfWeekList, dayOfMonth, nthWeekdayOfMonth, daily, workday, manual or external. Fields that mean nothing for the type are absent rather than defaulted, so test for presence rather than for a zero.
The time zone the schedule was configured with, as its own identifier (Europe/Rome). Never rewritten to the tzdb canonical name a linked city shares.
When the schedule starts, in its own time zone.
How many units of the schedule’s own kind sit between two hand-overs.
The countries whose weekends and holidays the skips are read from.
The next hand-overs this rotation has already scheduled. For anything further out, ask for a window from GET /v1/rotations/{rotationId}/schedule.
Set for dayOfWeek and nthWeekdayOfMonth.
Set for daysOfWeekList.
Set for dayOfMonth.
Set for nthWeekdayOfMonth: which occurrences in the month, 1 through 5.
Set for daily: the times of day it hands over at.
Set for external: which partner drives the rotation. One of pagerDuty, opsGenie, or none when the rotation is external but no partner is linked yet.
When the rotation is covered, and whether it is covered right now.
object
Whether business hours are switched on. Switching them off keeps the coverage week, so it can be switched back on without re-entering it.
Whether business hours actually restrict the rotation. This differs from enabled when the switch is on but the coverage week is empty, which restricts nothing.
The repeating week, as stored.
One stretch of the coverage week, which may run past midnight into the next day.
object
A weekday name, monday through sunday.
A time of day, HH:mm.
Whether days the schedule skips (weekends, national holidays) count as off time.
Whether the rotation is inside its hours at the moment of the response.
The Slack user group kept in sync with whoever is on call.
The on-call resource, embedded, so the common question needs no second call.
object
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”.
One person holding the shift, and when they hold it until.
object
The Slack user id of the person actually on duty.
Their position in the rotation’s turn order.
Members the plan would otherwise have named, who were passed over because they were absent.
A member the rotation passed over because they were away.
object
The Slack user id of the member who was passed over.
Where the absence came from: googleCalendar or internalAvailability. Null on older records, which predate the field, so treat it as optional.
Set when an external schedule put somebody on duty who does not map to a Slack user. For those rotations this is the only answer there is, so a client that reads onCall alone reports “nobody” for every one of them.
object
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”.
A Slack user, as Round Robin knows them.
object
The Slack user id.
The workspace that owns this person’s directory entry, which under Enterprise Grid can differ from the calling key’s workspace.
Null when the install was never granted users:read.email, not only when unknown.
A Slack Connect person: visible through a shared channel, never a member of the workspace.
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.
The person’s Slack avatar, at 192px.
The person’s Slack time zone, as an IANA identifier (Europe/Rome). Absent when Round Robin has not seen one for them.
Examplegenerated
{ "id": "example", "name": "example", "description": "example", "code": "example", "enabled": true, "public": true, "mode": "example", "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "members": { "users": [ { "userId": "example", "sequenceNumber": 1 } ], "userGroups": [ { "userGroupId": "example", "users": [ { "userId": "example", "sequenceNumber": 1 } ] } ] }, "plan": { "items": [ { "userId": "example", "userGroupId": "example", "turn": 1 } ] }, "schedule": { "type": "example", "timeZone": "example", "startsAt": "2026-04-15T12:00:00Z", "period": 1, "skipWeekends": true, "skipNationalHolidays": true, "countryCodes": [ "example" ], "nextEvents": [ "2026-04-15T12:00:00Z" ], "dayOfWeek": "example", "daysOfWeek": [ "example" ], "dayOfMonth": 1, "weeksInMonth": [ 1 ], "times": [ "example" ], "partner": "example" }, "businessHours": { "enabled": true, "inEffect": true, "timeZone": "example", "coverage": [ { "startDay": "example", "startTime": "example", "endDay": "example", "endTime": "example" } ], "considerScheduleSkipsAsOffTime": true, "isInBusinessHours": true, "currentBusinessHours": { "startUtc": "2026-04-15T12:00:00Z", "endUtc": "2026-04-15T12:00:00Z" }, "nextBusinessHour": { "startUtc": "2026-04-15T12:00:00Z", "endUtc": "2026-04-15T12:00:00Z" } }, "onDutyUserGroupId": "example", "onCall": { "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" } ] }, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z"}Created
A rotation: who is in it, when it hands over, and who holds the shift right now.
object
The short code the rotation answers to in Slack.
Whether every member of the workspace can see the rotation.
manual, auto or external.
How many people hold the shift at once.
Everybody in the rotation, split by how they got there.
object
Members added individually.
One member of a rotation.
object
Where the member sits in the order the plan is built from.
Members that came in through a Slack user group, listed under the group they came from. Add or remove somebody in Slack’s group and the rotation follows.
A Slack user group in the rotation, and the people currently in it.
object
One member of a rotation.
object
Where the member sits in the order the plan is built from.
The turn order.
object
One position in the turn order.
object
Set when the member is in the plan by way of a user group.
When the rotation hands over. Read type first: it says which of the fields below carry anything.
object
One of dayOfWeek, daysOfWeekList, dayOfMonth, nthWeekdayOfMonth, daily, workday, manual or external. Fields that mean nothing for the type are absent rather than defaulted, so test for presence rather than for a zero.
The time zone the schedule was configured with, as its own identifier (Europe/Rome). Never rewritten to the tzdb canonical name a linked city shares.
When the schedule starts, in its own time zone.
How many units of the schedule’s own kind sit between two hand-overs.
The countries whose weekends and holidays the skips are read from.
The next hand-overs this rotation has already scheduled. For anything further out, ask for a window from GET /v1/rotations/{rotationId}/schedule.
Set for dayOfWeek and nthWeekdayOfMonth.
Set for daysOfWeekList.
Set for dayOfMonth.
Set for nthWeekdayOfMonth: which occurrences in the month, 1 through 5.
Set for daily: the times of day it hands over at.
Set for external: which partner drives the rotation. One of pagerDuty, opsGenie, or none when the rotation is external but no partner is linked yet.
When the rotation is covered, and whether it is covered right now.
object
Whether business hours are switched on. Switching them off keeps the coverage week, so it can be switched back on without re-entering it.
Whether business hours actually restrict the rotation. This differs from enabled when the switch is on but the coverage week is empty, which restricts nothing.
The repeating week, as stored.
One stretch of the coverage week, which may run past midnight into the next day.
object
A weekday name, monday through sunday.
A time of day, HH:mm.
Whether days the schedule skips (weekends, national holidays) count as off time.
Whether the rotation is inside its hours at the moment of the response.
The Slack user group kept in sync with whoever is on call.
The on-call resource, embedded, so the common question needs no second call.
object
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”.
One person holding the shift, and when they hold it until.
object
The Slack user id of the person actually on duty.
Their position in the rotation’s turn order.
Members the plan would otherwise have named, who were passed over because they were absent.
A member the rotation passed over because they were away.
object
The Slack user id of the member who was passed over.
Where the absence came from: googleCalendar or internalAvailability. Null on older records, which predate the field, so treat it as optional.
Set when an external schedule put somebody on duty who does not map to a Slack user. For those rotations this is the only answer there is, so a client that reads onCall alone reports “nobody” for every one of them.
object
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”.
A Slack user, as Round Robin knows them.
object
The Slack user id.
The workspace that owns this person’s directory entry, which under Enterprise Grid can differ from the calling key’s workspace.
Null when the install was never granted users:read.email, not only when unknown.
A Slack Connect person: visible through a shared channel, never a member of the workspace.
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.
The person’s Slack avatar, at 192px.
The person’s Slack time zone, as an IANA identifier (Europe/Rome). Absent when Round Robin has not seen one for them.
Examplegenerated
{ "id": "example", "name": "example", "description": "example", "code": "example", "enabled": true, "public": true, "mode": "example", "dutySize": 1, "owners": [ "example" ], "channels": [ "example" ], "members": { "users": [ { "userId": "example", "sequenceNumber": 1 } ], "userGroups": [ { "userGroupId": "example", "users": [ { "userId": "example", "sequenceNumber": 1 } ] } ] }, "plan": { "items": [ { "userId": "example", "userGroupId": "example", "turn": 1 } ] }, "schedule": { "type": "example", "timeZone": "example", "startsAt": "2026-04-15T12:00:00Z", "period": 1, "skipWeekends": true, "skipNationalHolidays": true, "countryCodes": [ "example" ], "nextEvents": [ "2026-04-15T12:00:00Z" ], "dayOfWeek": "example", "daysOfWeek": [ "example" ], "dayOfMonth": 1, "weeksInMonth": [ 1 ], "times": [ "example" ], "partner": "example" }, "businessHours": { "enabled": true, "inEffect": true, "timeZone": "example", "coverage": [ { "startDay": "example", "startTime": "example", "endDay": "example", "endTime": "example" } ], "considerScheduleSkipsAsOffTime": true, "isInBusinessHours": true, "currentBusinessHours": { "startUtc": "2026-04-15T12:00:00Z", "endUtc": "2026-04-15T12:00:00Z" }, "nextBusinessHour": { "startUtc": "2026-04-15T12:00:00Z", "endUtc": "2026-04-15T12:00:00Z" } }, "onDutyUserGroupId": "example", "onCall": { "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" } ] }, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z"}Bad Request
object
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example"}Unauthorized
object
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example"}Forbidden
object
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example"}Not Found
object
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example"}Conflict
object
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example"}Precondition Required
object
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example"}