The rota
The rota is a circle's duty roster: duties (cleaning the common house, cooking on Thursdays), the roster of
people in the draw for each duty, and shifts (a duty on certain days) with their seats. These endpoints let an
admin's assistant install the rota on a circle and set it up. Read Start here first: its rules apply to
everything on this page. Duties and shifts work like Events and Posts where it comes to
external_id and re-runs, and this page says where they differ.
The rota comes last in a move: its roster and seats are real members, so the people must have joined, and been matched, first (see Members).
What the API has, and what it does not
- Install the rota on a circle, and ask whether it is there.
- Duties, with their roster (who is in the draw) and their shifts.
- Shifts, with their seats (who is on duty, and who leads).
- Not in the API: uninstalling the rota (the admin does that in the app), swap requests between members, and checking off a shift's checklist. There is no bulk or repeat endpoint either: shifts are created one by one.
- Shifts and seats in the past are allowed, and so is any date from 1 January 1990 to 50 years ahead.
- Author claims do not cover the rota. A duty or shift has no author, so there is nothing to hand to a member.
Everything sits under $API, and a person is always named by their member_id: the member's text id from
Members, such as EveABqLyto9L, never a database number.
Install it: POST /circles/:circle_id/rota
curl -X POST "$API/circles/412/rota" -H "Authorization: Bearer $KEY"
The rota can only be installed by someone who may install an extension on that circle in the app: the circle's owner or admin, or an owner or admin of the community (also on a sub-circle they are not in). The answers:
| Answer | Meaning |
|---|---|
404 not_found |
The circle is not one of this community's. |
403 forbidden |
The admin may not install extensions on that circle. |
422 rota_not_available |
The rota is not offered to this community (it is not in the community's directory). Nothing was installed, also when an installation exists. Tell the admin. |
409 rota_suspended |
The rota is installed but switched off on that circle. Switching it on again is done in the app. |
200 OK |
It was already installed and live. Nothing changed. |
201 Created |
Installed now. |
Installing tells nobody. Both 200 and 201 answer:
{"circle_id": 412, "created_at": "2026-11-02T09:00:00.000Z", "can_manage": true}
can_manage says whether the admin may run the rota there. Installing and running are not the same right: an
organisation admin who is not in a sub-circle can install the rota on it, but only people who are admins of that
circle in the app can run it (can_manage is false for the key's admin, and the writes below answer 403). Ask the
admin to join the sub-circle in the app first.
Read it: GET /circles/:circle_id/rota
curl "$API/circles/412/rota" -H "Authorization: Bearer $KEY"
Answers 200 OK with the object above, 404 not_found when the rota is not installed (or not live) on that circle,
and 403 forbidden when the admin may neither see the rota nor install it.
Reach. Reading follows the app: only a member of the circle sees its rota, and the key never widens that. Writing
needs an admin of the circle. Anything outside this community, or on a circle without a live rota, is 404 not_found;
what the admin may not see or do is 403 forbidden.
Duties
{
"id": 61,
"external_id": "old-duty-3",
"circle_id": 412,
"name": "Cooking on Thursdays",
"description": "Dinner for about twenty.",
"reminder_days": 1,
"checklist": "Check the fridge\nShop on Wednesday",
"position": 1,
"created_at": "2026-11-02T09:05:00.000Z"
}
| Field | Rules |
|---|---|
name |
Required on a create: text of 1 to 60 characters. Never cleared. |
description |
Text of at most 500 characters. null clears it. |
reminder_days |
A whole number of days from 1 to 14: how long before a shift its people are reminded. Default 1. |
checklist |
Text of at most 2,000 characters. null clears it. |
external_id |
Your id for the duty on the old platform. Always send it. |
curl "$API/rota/duties" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"circle_id": 412, "external_id": "old-duty-3",
"name": "Cooking on Thursdays",
"description": "Dinner for about twenty.",
"reminder_days": 1}'
POST /rota/dutiesneedscircle_idin the body.201 Createdwith the duty; a re-run is described below.GET /rota/duties?circle_id=412lists a circle's duties, ordered byposition(the order the app shows them in). Withoutcircle_idit lists the duties of every circle of the community whose rota the admin can read, one circle after another, each circle's inpositionorder. Answers{"data": [<duty>, …], "next_page": null}, 100 per page.GET /rota/duties/:idreads one.PATCH /rota/duties/:idchanges the fields you send, and leaves the others as they are.DELETE /rota/duties/:iddeletes it.
curl "$API/rota/duties?circle_id=412" -H "Authorization: Bearer $KEY"
curl -X PATCH "$API/rota/duties/61" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"reminder_days": 2}'
Warning: deleting a duty deletes its shifts, its seats and its roster too. There is no undo. Never send a DELETE without the admin's clear yes, saying how many shifts go with it.
curl -X DELETE "$API/rota/duties/61" -H "Authorization: Bearer $KEY"
Answers 204 No Content. When someone edits a duty in the app, a later re-run of it answers
409 changed_since_import (below). A PATCH through the API still applies.
Shifts
A shift is a duty on certain days, with a number of seats.
{
"id": 905,
"external_id": "old-shift-2026-11-06",
"duty_id": 61,
"starts_on": "2026-11-06",
"ends_on": "2026-11-06",
"seats": 2,
"free_seats": 1,
"description": "Soup and bread.",
"assignments": [
{
"member_id": "EveABqLyto9L",
"name": "Karen Holm",
"still_member": true,
"lead": true,
"shift_id": 905,
"created_at": "2026-11-02T09:10:00.000Z"
}
],
"created_at": "2026-11-02T09:06:00.000Z"
}
| Field | Rules |
|---|---|
duty_id |
Required on a create: the duty it belongs to. A shift stays on the duty it was made on; neither a PATCH nor a re-run can move it. |
starts_on |
Required on a create: a plain date, 2026-11-06. A time or a date that does not exist is 422. |
ends_on |
A plain date, not before starts_on. Defaults to starts_on: a shift sent without an end is a one-day shift, also on a re-run that leaves it out. A PATCH that sends only starts_on keeps the stored ends_on. |
seats |
A whole number from 1 to 50. Default 1. It cannot go below the seats already taken. |
description |
Text of at most 500 characters. null clears it. |
external_id |
Your id for the shift on the old platform. Always send it. |
The dates are plain dates, with no time and no time zone. They may be in the past: the API has no past check, as the admin has none in the app.
curl "$API/rota/shifts" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"duty_id": 61, "external_id": "old-shift-2026-11-06",
"starts_on": "2026-11-06", "seats": 2,
"description": "Soup and bread."}'
POST /rota/shiftsanswers201 Createdwith the shift (seats and all). Make shifts one by one, each with itsexternal_id; there is no repeat or bulk endpoint, and a run that stops halfway is simply sent again.GET /rota/shifts?duty_id=61lists a duty's shifts.duty_idis required. They run oldest first, the schedule's order.?from=2026-11-01keeps the shifts that end on or after that date, which is how to ask for "from today on". Answers{"data": […], "next_page": null}, 100 per page.GET /rota/shifts/:idreads one.PATCH /rota/shifts/:idchanges the fields you send.DELETE /rota/shifts/:iddeletes it, with its seats.
curl "$API/rota/shifts?duty_id=61&from=2026-11-01" -H "Authorization: Bearer $KEY"
curl -X PATCH "$API/rota/shifts/905" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"seats": 3}'
Moving a shift's dates (a PATCH or a re-run that changes starts_on or ends_on) cancels any swap request on
it that the new dates void, and tells the member who asked for it: see "Nobody is told". Deleting a shift deletes any
hand-over request on it at once, and tells nobody. A swap of shifts (a trade) whose other shift survives is cancelled
later by the app's own hourly clean-up, which tells the member who asked (the key's holder too, if they asked: the
clean-up knows no actor).
Warning: deleting a shift removes its seats for the people on it. Never send a DELETE without the admin's clear yes. There is no undo.
curl -X DELETE "$API/rota/shifts/905" -H "Authorization: Bearer $KEY"
Answers 204 No Content. 422 invalid with details.seats when the seats asked for are fewer than those taken.
The roster: who is in the draw for a duty
A duty's roster lists the members the app offers when someone is seated or draws a shift. Taking a person off the roster keeps the seats they already hold.
{
"member_id": "EveABqLyto9L",
"name": "Karen Holm",
"still_member": true,
"created_at": "2026-11-02T09:07:00.000Z"
}
curl "$API/rota/duties/61/participants" -H "Authorization: Bearer $KEY"
curl "$API/rota/duties/61/participants" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"member_id": "EveABqLyto9L"}'
curl -X DELETE "$API/rota/duties/61/participants/EveABqLyto9L" \
-H "Authorization: Bearer $KEY"
GETlists the roster,{"data": […], "next_page": null}.POSTadds a person:201 Createdwith the row, or200 OKwith the existing row when they are already on it (nothing changes).DELETE …/participants/:member_idtakes a person off:204 No Content.
Only current members of the circle can be added. Anyone else (an unknown id, someone who has not joined, someone
who has left the circle) is 422 with the code not_a_member and details.member_id. The API never drops such a
person quietly, the way the app's roster form does: report them to the admin instead. A person who has left the circle
is still listed, with "still_member": false and no name, and can still be removed by their member_id.
Seats: who is on a shift
A seat names a member on a shift, and may mark them as the lead. Seats are addressed by member_id, under the shift:
{
"member_id": "EveABqLyto9L",
"name": "Karen Holm",
"still_member": true,
"lead": true,
"shift_id": 905,
"created_at": "2026-11-02T09:10:00.000Z"
}
curl "$API/rota/shifts/905/assignments" -H "Authorization: Bearer $KEY"
curl "$API/rota/shifts/905/assignments" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"member_id": "EveABqLyto9L", "lead": true}'
curl -X PATCH "$API/rota/shifts/905/assignments/EveABqLyto9L" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"lead": false}'
curl -X DELETE "$API/rota/shifts/905/assignments/EveABqLyto9L" \
-H "Authorization: Bearer $KEY"
GET …/assignmentslists the seats andGET …/assignments/:member_idreads one.POSTseats a person:201 Createdwith the seat. Someone who already has a seat on the shift answers200 OKwith it, and nothing changes and nobody is told.PATCHsetsleadon a seat that exists (404 not_foundotherwise).leadis required and is the only field.DELETEtakes the person off the shift:204 No Content.
The rules:
- Seat only real members. An unknown id or a person who is not a current member of the circle is
422 not_a_member(details.member_id). APATCHwith"lead": trueon the seat of someone who has since left is the same422 not_a_member. - A full shift answers
422 no_free_seat. Raise the shift'sseatsfirst. The last seat goes to one caller only. - One lead.
"lead": trueon a create orPATCHmoves the lead to that person."lead": falseon the lead leaves the shift with no lead; on anyone else it changes nothing. APATCHwithoutleadis422 invalid. - A lead change never cancels a swap request. A seat that is added or removed can (see below).
- People who left. A seat whose person has left the circle is listed with
"still_member": false, and can be removed bymember_id. Its lead mark stays unless it was theirs.
Re-runs
Duties and shifts have an external_id, and follow Re-runs and external_id like
everything else:
| Answer | Meaning |
|---|---|
201 Created |
New. |
200 OK |
Already imported. It was updated with what you send and keeps its id. A shift sent without ends_on gets ends_on = starts_on again. |
409 record_deleted |
The duty or shift was deleted since (also when the rota was uninstalled). It is never brought back. Skip it and report it. |
409 changed_since_import |
Someone edited that duty or shift in the app since your last write (an admin's edit, or a member changing its description). It was not overwritten. Skip it and report it. |
409 conflict |
The same create arrived twice at once. Send it again, once. |
403 forbidden / 404 not_found |
The admin can no longer run the rota there, or the rota is not live any more. Skip it and report it. |
Seats and roster rows never make a duty or a shift "changed": the question is only whether a person edited the duty or the shift itself.
Roster rows and seats have no external_id. Their natural key is the pair: a person on a duty's roster, a person
on a shift. Sending the same one again answers 200 OK with the row that exists, and changes nothing (a lead sent
with it is ignored, use PATCH). A run that stops halfway is sent again from the start.
Who may do what
| What | Needs |
|---|---|
| Install the rota | The right to install extensions on the circle: its owner or admin, or an owner or admin of the community. |
| Read it (the duties, shifts, roster and seats) | Being a member of the circle, as in the app. |
| Run it (every write on this page) | Being an admin of the circle, as in the app. |
An organisation admin who is not in a sub-circle can install its rota (and ask GET /circles/:id/rota), but
can_manage is false and they can neither read nor run it until they join the sub-circle. The key never does more
than the admin could in the app.
Nobody is told
Every rota write is silent: installing, creating, changing and deleting duties and shifts, roster rows, and seats make no bell, no push message and no email for anyone, and nothing in a daily summary. There are three exceptions, all the app's own behaviour:
notify: trueon a seat create sends the app's "You are on: " notice to the person just seated, as when an admin seats someone in the app. Never to the key's holder, and never for a seat that already existed (200). Send it only when the admin has asked for people to be told. Withoutnotify, the create is silent. The notice goes only to a member of the circle; the API refuses anything else anyway.- A reseat or a date move that cancels a person's swap request tells them. If a member had asked to swap a shift,
and you remove a seat, add one that voids the request, or move the shift's dates, the request lapses and the app's
usual urgent notice goes to the member who made it, as the app does. Not to the key's holder about their own
request, and a lead change never does it. This is the only notice a write sends without
notify, and it cannot be switched off: check with the admin before reseating or moving a shift people have asked to swap. - Deleting a shift deletes a hand-over request on it at once, and tells nobody. Only a pending swap of shifts (a trade) whose other shift survives lapses later, when the app's hourly clean-up tells the member who asked (with no actor, so the key's holder too if they asked).
Reminders work as normal. A seat on a future shift gets the duty's usual reminder (reminder_days before it) once,
to the person seated, according to their own settings. That is the rota working, not something an import sends. A seat
on a past shift never gets a reminder: its time is over, so importing history tells no one.