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/duties needs circle_id in the body. 201 Created with the duty; a re-run is described below.
  • GET /rota/duties?circle_id=412 lists a circle's duties, ordered by position (the order the app shows them in). Without circle_id it lists the duties of every circle of the community whose rota the admin can read, one circle after another, each circle's in position order. Answers {"data": [<duty>, …], "next_page": null}, 100 per page.
  • GET /rota/duties/:id reads one.
  • PATCH /rota/duties/:id changes the fields you send, and leaves the others as they are.
  • DELETE /rota/duties/:id deletes 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/shifts answers 201 Created with the shift (seats and all). Make shifts one by one, each with its external_id; there is no repeat or bulk endpoint, and a run that stops halfway is simply sent again.
  • GET /rota/shifts?duty_id=61 lists a duty's shifts. duty_id is required. They run oldest first, the schedule's order. ?from=2026-11-01 keeps 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/:id reads one.
  • PATCH /rota/shifts/:id changes the fields you send.
  • DELETE /rota/shifts/:id deletes 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"
  • GET lists the roster, {"data": […], "next_page": null}.
  • POST adds a person: 201 Created with the row, or 200 OK with the existing row when they are already on it (nothing changes).
  • DELETE …/participants/:member_id takes 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 …/assignments lists the seats and GET …/assignments/:member_id reads one.
  • POST seats a person: 201 Created with the seat. Someone who already has a seat on the shift answers 200 OK with it, and nothing changes and nobody is told.
  • PATCH sets lead on a seat that exists (404 not_found otherwise). lead is required and is the only field.
  • DELETE takes 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). A PATCH with "lead": true on the seat of someone who has since left is the same 422 not_a_member.
  • A full shift answers 422 no_free_seat. Raise the shift's seats first. The last seat goes to one caller only.
  • One lead. "lead": true on a create or PATCH moves the lead to that person. "lead": false on the lead leaves the shift with no lead; on anyone else it changes nothing. A PATCH without lead is 422 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 by member_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: true on 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. Without notify, 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.