# Green Lane Scouts 73: Admin API _Tested and approved 2026-09-05. Every route below was driven over HTTP on a throwaway copy of the live database with four levels of access (anonymous, member, unit leader, admin, plus the site owner where a route is owner-only): 123 checks, 0 failures. The calendar checks ran against the real store with 2036 probes and left it as found. The live registry is `GET /api/docs`; this file records what each route actually did when asked. Regenerate with `tests/api_drive.py` then `tests/api_doc.py`._ **Base:** `https://greenlanescouts73.org`. **Auth:** a browser session (cookie `s73_session`) or a bearer API key (`Authorization: Bearer gls73_...`), honoured on `/api/admin` only. `401` means no credential; `403` means a credential without the capability. Error bodies are `{"detail": ...}` or, from a store guardrail, `{"detail": {"error": ...}}`. Every write lands in the action log with who did it and what changed. Times are ISO 8601 with an offset. ## Identity Who is calling, and the live route registry. ### `GET /api/admin/whoami` Any credential; answers who you are, how you authenticated, and your capabilities. | Tried | HTTP | |---|---| | anon | 401 | | claude session | 200 | | member session | 200 | | admin token from LAN | 200 | | admin token from WAN | 403 | | disabled person's session is dead | 401 | | fresh member session after enable | 200 | ### `GET /api/docs` Capability `api:docs`, leader and above. Generated from the router on every request. | Tried | HTTP | |---|---| | anon | 401 | | member (no api:docs) | 403 | | leader | 200 | ## Summary and leads Join-form leads are read-only. A claim is who has a lead and when; it is the only thing that changes. ### `GET /api/admin/summary` Capability `leads:read`. | Tried | HTTP | |---|---| | anon | 401 | | member | 403 | | leader | 200 | ### `GET /api/admin/leads` Capability `leads:read`. | Tried | HTTP | |---|---| | leader | 200 | | search miss | 200 | | bearer key works | 200 | | bearer key from WAN while lan | 403 | | revoked key dead | 401 | ### `GET /api/admin/leads/{id}` Capability `leads:read`. | Tried | HTTP | |---|---| | one lead | 200 | | unknown lead | 404 | ### `POST /api/admin/leads/{id}/claim` Capability `leads:read`. Take a lead so it is not sitting unclaimed. Outreach v1 is exactly | Tried | HTTP | |---|---| | claim | 200 | | claim again (self) | 409 | | claim by another | 409 | ### `POST /api/admin/leads/{id}/release` Capability `leads:read`. Let a lead go. The holder may; so may an owner (people:manage), | Tried | HTTP | |---|---| | release by another admin (no people:manage) | 403 | | release by owner | 200 | | release when nobody has it | 409 | ### `GET /api/admin/mirrors/failed` Capability `leads:read`. | Tried | HTTP | |---|---| | failed mirrors | 200 | ### `POST /api/admin/mirrors/retry` Capability `leads:read`. Replay leads whose copy to an external target failed. Idempotent-ish: | Tried | HTTP | |---|---| | retry sheet (nothing owed) | 200 | | retry ntfy (not implemented) | 422 | ## Announcements Site banners. `ends_at` is required. Revoke keeps the row. ### `GET /api/admin/announcements` Capability `announcements:write`. Every announcement with its computed state: live, scheduled, expired, | Tried | HTTP | |---|---| | list | 200 | ### `POST /api/admin/announcements` Capability `announcements:write`. Post a notice. ends_at is required. | Tried | HTTP | |---|---| | no ends_at | 422 | | create (2036) | 201 | | member cannot | 403 | ### `DELETE /api/admin/announcements/{id}` Capability `announcements:write`. Take one down early. Sets revoked_at; never deletes the row. | Tried | HTTP | |---|---| | revoke | 200 | | revoke again | 409 | | revoke unknown | 404 | ## Nearby units The find-a-unit directory. Deactivate, never delete. Saving bumps `verified_at`. ### `GET /api/admin/nearby` Capability `nearby:write`. The editing view, so deactivated rows are reachable. nearby:write | Tried | HTTP | |---|---| | list | 200 | | bearer key out of scope | 403 | ### `POST /api/admin/nearby` Capability `nearby:write`. | Tried | HTTP | |---|---| | bad unit_type | 422 | | create | 201 | ### `PATCH /api/admin/nearby/{id}` Capability `nearby:write`. Partial update. Saving bumps verified_at to today unless the payload | Tried | HTTP | |---|---| | update | 200 | | unknown field rejected | 422 | | unknown | 404 | ### `DELETE /api/admin/nearby/{id}` Capability `nearby:write`. Take a unit off the page. Sets active=0; never deletes the row. | Tried | HTTP | |---|---| | deactivate | 200 | | deactivate again | 409 | ## Our units Meeting day, time and place. A unit leader edits their own unit only. ### `GET /api/admin/units` Capability `unit:write_own`. The units the caller may edit, which is what the screen this feeds | Tried | HTTP | |---|---| | pack leader sees only the pack | 200 | | admin sees both | 200 | ### `PATCH /api/admin/units/{slug}` Capability `unit:write_own`. | Tried | HTTP | |---|---| | pack leader cannot edit the troop | 403 | | bad time | 422 | | no-change save | 200 | | unknown | 404 | ## Settings Typed keys only; `value: null` clears to the code default. ### `GET /api/admin/settings` Capability `settings:write`. | Tried | HTTP | |---|---| | leader cannot | 403 | | admin | 200 | ### `PUT /api/admin/settings/{key}` Capability `settings:write`. Set one value, or clear it back to the code default with value: null. | Tried | HTTP | |---|---| | bad value | 422 | | set | 200 | | clear to default | 200 | | unknown key | 422 | ## API keys Bearer keys, honoured on `/api/admin` only. Scopes are a subset of the owner's capabilities; a key can never mint keys. LAN-only unless `api_keys_from` is `anywhere`. ### `GET /api/admin/keys` Capability `apikeys:own`. Your keys, newest first, with state (active / expired / revoked) and | Tried | HTTP | |---|---| | admin token cannot own keys | 403 | | own list | 200 | ### `POST /api/admin/keys` Capability `apikeys:own`. Mint a key. Body: label, scopes (list), expires_days (optional, 1-365). | Tried | HTTP | |---|---| | unscopable scope | 422 | | mint | 201 | | a key cannot mint keys | 403 | ### `DELETE /api/admin/keys/{id}` Capability `apikeys:own`. Revoke one of your keys. The row stays. A key that is not yours is | Tried | HTTP | |---|---| | revoke | 200 | | revoke again | 409 | | foreign/unknown key | 404 | ## History The action log, newest first. Admin and above. ### `GET /api/admin/history` Capability `history:read`. Newest first. `kind` is a prefix filter (calendar., nearby., login.). | Tried | HTTP | |---|---| | leader cannot | 403 | | admin, filtered | 200 | ## People Invites and reset links are returned once and handed over by the admin; there is no outbound mail. Grants are limited to what the caller holds. Owner-only: roles, disable, enable. ### `GET /api/admin/people` Capability `people:invite_leader`. Everyone with an account, their roles and memberships, active | Tried | HTTP | |---|---| | leader cannot | 403 | | admin | 200 | ### `POST /api/admin/people/invite` Capability `people:invite_leader`. Mint an invite. Body: email, global_role (optional), units | Tried | HTTP | |---|---| | bad email | 422 | | admin cannot grant owner | 403 | | invite | 201 | ### `DELETE /api/admin/people/invite/{id}` Capability `people:invite_leader`. | Tried | HTTP | |---|---| | revoke invite | 200 | | revoke again | 404 | ### `PUT /api/admin/people/{id}/roles` Capability `people:manage`. Replace a person's global role and unit memberships. Body: | Tried | HTTP | |---|---| | admin cannot change roles (owner only) | 403 | | owner sets roles | 200 | ### `POST /api/admin/people/{id}/disable` Capability `people:manage`. Disable a person now: sessions end, keys stop, documents close. The | Tried | HTTP | |---|---| | admin cannot disable | 403 | | owner disables | 200 | ### `POST /api/admin/people/{id}/enable` Capability `people:manage`. | Tried | HTTP | |---|---| | owner enables | 200 | ### `POST /api/admin/people/{id}/reset` Capability `people:invite_admin`. Mint a one-time password-reset link (24 h), returned ONCE. Their | Tried | HTTP | |---|---| | admin mints a reset link | 201 | | owner may reset an admin | 201 | | unknown person | 404 | ## Roster and family Families, scouts, and a per-year checklist recording that a thing was collected, never the thing. Nothing medical is stored. `/family` is the signed-in parent's own households. ### `GET /api/admin/roster` Capability `roster:write`. Households with their scouts and this year's checklist. `unit` is a | Tried | HTTP | |---|---| | member cannot | 403 | | roster by unit | 200 | ### `POST /api/admin/roster/households` Capability `roster:write`. A family. Body: parent_name (required), email, phone, second_parent, | Tried | HTTP | |---|---| | no parent | 422 | | create family | 201 | | import lead | 201 | | import twice | 409 | ### `GET /api/admin/roster/households/{id}` Capability `roster:write`. | Tried | HTTP | |---|---| | one family | 200 | ### `PATCH /api/admin/roster/households/{id}` Capability `roster:write`. | Tried | HTTP | |---|---| | deactivate family | 200 | ### `POST /api/admin/roster/households/{id}/scouts` Capability `roster:write`. Body: first_name (required), last_name, unit_id (required), den, bsa_member_id. | Tried | HTTP | |---|---| | add scout | 201 | | bad bsa id | 422 | ### `PUT /api/admin/roster/households/{id}/people` Capability `people:invite_leader`. Which accounts belong to this family: body {person_ids: [...]}. This is | Tried | HTTP | |---|---| | leader cannot link accounts | 403 | | unknown person id | 422 | | admin links the member | 200 | ### `PATCH /api/admin/roster/scouts/{id}` Capability `roster:write`. | Tried | HTTP | |---|---| | edit scout | 200 | ### `PUT /api/admin/roster/scouts/{id}/checks/{item}` Capability `roster:write`. Mark an item collected for this year: body {done: true|false, note}. | Tried | HTTP | |---|---| | mark dues | 200 | | bad item | 422 | ### `GET /api/admin/family` Capability `account:self`. The signed-in person's own families: scouts, dens, and this year's | Tried | HTTP | |---|---| | member sees own family | 200 | | unlinked account sees none | 200 | | inactive family drops off | 200 | | anon | 401 | ## Calendar Writes go to Radicale. The site owns its two UID namespaces and refuses any other before a network call. DELETE really deletes. ### `GET /api/admin/calendar` Capability `calendar:write`. Every event the public calendar shows, newest first is NOT the order: | Tried | HTTP | |---|---| | member cannot | 403 | | list | 200 | ### `POST /api/admin/calendar` Capability `calendar:write`. Add an event. Body: title, date (YYYY-MM-DD), unit (pack|troop|both), | Tried | HTTP | |---|---| | no title | 422 | | create (2036) | 201 | ### `PUT /api/admin/calendar/{uid}` Capability `calendar:write`. Replace one site-owned event in full (same body as POST). A UID the | Tried | HTTP | |---|---| | replace | 200 | | foreign uid | 403 | ### `DELETE /api/admin/calendar/{uid}` Capability `calendar:write`. Remove one site-owned event from the store. Unlike everything else on | Tried | HTTP | |---|---| | delete | 200 | | delete again | 404 | ## Facebook posts The publisher reports; leaders read, cancel and edit. The image hash is verified on arrival and the image route is gated like the list. Cancel and reschedule go through the publisher's signed per-post link. ### `GET /api/admin/fbposts` Capability `fbposts:read`. Every post the publisher has reported, newest scheduled first, with | Tried | HTTP | |---|---| | member cannot | 403 | | listed as scheduled | 200 | | published dropped from open list | 200 | ### `POST /api/admin/fbposts/ingest` Capability `fbposts:ingest`. scout-publisher reports a post. Body: id (/), unit, | Tried | HTTP | |---|---| | leader cannot ingest | 403 | | hash mismatch refused | 422 | | ingest with image | 200 | | past time reads as published | 200 | ### `GET /api/admin/fbposts/{id}/image` Capability `fbposts:read`. The stored image, behind the same gate as the list. | Tried | HTTP | |---|---| | image anon | 401 | | image gated | 200 | ### `POST /api/admin/fbposts/{id}/cancel` Capability `fbposts:read`. Cancel a scheduled post through the publisher's own signed link, then | Tried | HTTP | |---|---| | cancel: publisher refuses a bad signature | 502 | | cancel after time passed | 409 | ### `POST /api/admin/fbposts/{id}/reschedule` Capability `fbposts:read`. Edit and repost a scheduled post: body {message, scheduled_for}. The | Tried | HTTP | |---|---| | reschedule: time too soon | 422 | | reschedule: publisher refuses a bad signature | 502 |