{"openapi":"3.1.0","info":{"title":"Roost v2 / Pigeonly Company OS","version":"0.1.0"},"paths":{"/api/platform/constitution":{"get":{"tags":["platform"],"summary":"List Constitution","operationId":"list_constitution_api_platform_constitution_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Constitution Api Platform Constitution Get"}}}}}}},"/api/platform/action-items":{"get":{"tags":["platform"],"summary":"List Action Items","operationId":"list_action_items_api_platform_action_items_get","parameters":[{"name":"assignee","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Assignee"}},{"name":"band","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Band"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"include_all_pools","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include All Pools"}},{"name":"include_pools","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Pools"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Action Items Api Platform Action Items Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/action-items/{item_id}/resolve":{"post":{"tags":["platform"],"summary":"Resolve Action Item","operationId":"resolve_action_item_api_platform_action_items__item_id__resolve_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resolve Action Item Api Platform Action Items  Item Id  Resolve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/action-items/{item_id}/dismiss":{"post":{"tags":["platform"],"summary":"Dismiss Action Item","description":"\"This does not need doing\", as distinct from resolve's \"this got\ndone\". Returns an undo token like resolve, because dismissing the wrong\nrow by mis-click has to be as recoverable as resolving one.","operationId":"dismiss_action_item_api_platform_action_items__item_id__dismiss_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DismissRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Dismiss Action Item Api Platform Action Items  Item Id  Dismiss Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/action-items/{item_id}/explain-delay":{"post":{"tags":["platform"],"summary":"Explain Action Item Delay","description":"The reason behind a late card, from the person it is assigned to.","operationId":"explain_action_item_delay_api_platform_action_items__item_id__explain_delay_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainDelayRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Explain Action Item Delay Api Platform Action Items  Item Id  Explain Delay Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/action-items/{item_id}/reassign":{"post":{"tags":["platform"],"summary":"Reassign Action Item","description":"The manual per-card reassign control for an unresolved-name pool\ncard (GitHub issue #2319, Frederick 2026-09-14, R-154;\nPLAN-ACTIVE-TODAY-CARDS-SETTLE-001.md §F). Same write gate as\nresolve/dismiss/undo above (`platform:action_items:write`) -- no new\nauthority (RA-01/RA-04, docs/00_business_rules/ROOSTER_AUTHORITY.md):\nreassignment within the org is a routine, reversible, in-house act, and\na pool card's own VISIBILITY already restricted who could ever reach\nthis button to whoever holds that pool's grant (see\naction_items.list_worked_pool_items/list_visible_pool_items).","operationId":"reassign_action_item_api_platform_action_items__item_id__reassign_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReassignRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reassign Action Item Api Platform Action Items  Item Id  Reassign Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/action-items/verify-resolutions":{"post":{"tags":["platform"],"summary":"Verify Action Item Resolutions","description":"Reconciles action items against reality in BOTH directions:\n\n  * Re-checks resolved items whose grace window has come due, and pushes\n    back onto the surface any whose condition is STILL true. Pressing\n    Resolve is a claim that the work happened; this is what checks it.\n  * Auto-resolves OPEN items in `action_item_verify.SETTLERS` whose\n    condition has now cleared (docs/OWNER_TODAY.md, Frederick,\n    2026-08-08: resolving must follow from the real work, not a click\n    that only hides the card -- see that module's own docstring).\n  * Closes OPEN items of a kind whose producer has been deleted\n    (`action_item_verify.RETIRED_KINDS`) -- residue no predicate can\n    ever settle, because nothing raises the condition any more.\n\nBoth are deterministic by construction -- \"is this invoice still\nunpaid\" has one right answer -- so no model is consulted either way.\nKinds outside the relevant map are untouched by that half of the sweep.","operationId":"verify_action_item_resolutions_api_platform_action_items_verify_resolutions_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Verify Action Item Resolutions Api Platform Action Items Verify Resolutions Post"}}}}}}},"/api/platform/action-items/{item_id}/undo":{"post":{"tags":["platform"],"summary":"Undo Action Item","operationId":"undo_action_item_api_platform_action_items__item_id__undo_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UndoRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Undo Action Item Api Platform Action Items  Item Id  Undo Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/live/events":{"get":{"tags":["live"],"summary":"Live Events","description":"Tells an open page that something new arrived. Nothing about what.\n\n⛔EVERY SCREEN IN THIS PRODUCT IS A RELOAD BUTTON. Frederick, 2026-09-04:\n\"they don't have to keep hitting a reload button ... maybe you get a\nlittle red dot when there's a new transcription.\"\n\n⛔AND THE STREAM CARRIES NO CONTENT, WHICH IS THE WHOLE SECURITY MODEL.\nAn event says a KIND of work arrived -- 'transcription', 'ticket_message'\n-- and nothing else: no name, no address, not even a row id. Every page\nthen refetches through the gated route it already had to be allowed to\ncall, so an open stream can never become a way around a permission.\nAuthentication is still required, because who is watching is worth\nknowing even when what they hear is nothing.\n\n`X-Accel-Buffering: no` is not decoration: nginx buffers proxied\nresponses by default, and a buffered stream delivers its first event when\nthe buffer fills -- which for events this small is never.","operationId":"live_events_api_live_events_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/platform/principals":{"get":{"tags":["platform-admin"],"summary":"List Principals","operationId":"list_principals_api_platform_principals_get","parameters":[{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kind"}},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},{"name":"include_removed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Removed"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Principals Api Platform Principals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["platform-admin"],"summary":"Create Principal","operationId":"create_principal_api_platform_principals_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrincipalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Principal Api Platform Principals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/names":{"get":{"tags":["platform-admin"],"summary":"List Principal Names","description":"Every principal's id and resolved name, unpaged -- the identical fix,\nfor the identical reason, as GET /api/orgs/organizations/names (see that\nendpoint's own docstring in app/modules/orgs/routes.py).\n\napps/web/src/lib/usePrincipalNameLookup.ts fetched `GET\n/principals?limit=200` for this; `MAX_PAGE_LIMIT = 200`\n(registry.clamp_pagination) clamps that endpoint server-side regardless\nof what the caller asks for. Only 32 principals exist today, so nothing\nhas actually fallen off that ceiling yet -- but that makes it a LATENT\ntruncation bug, not a fixed one, and this defect class has now shipped\nfour times (PR #205 for fulfillment/SlaPage.tsx, tickets' own org_name\nJOIN, /organizations/names above, now this). Fixed here before the\nroster ever crosses 200 rather than after.\n\nSame `admin:people:read` gate as GET /principals -- this does not widen\nwho can resolve a principal's name, only how completely. Resolves\nthrough `registry.resolved_name_sql` (preferred_name if the person set\none, else display_name -- see `_principal_to_dict`'s own note on this),\nthe one name resolution every read site is meant to route through, and\nexcludes removed principals (migration 0099 tombstone) the same way the\ndefault `GET /principals` list does.","operationId":"list_principal_names_api_platform_principals_names_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Principal Names Api Platform Principals Names Get"}}}}}}},"/api/platform/principals/{principal_id}":{"patch":{"tags":["platform-admin"],"summary":"Update Principal","operationId":"update_principal_api_platform_principals__principal_id__patch","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrincipalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Principal Api Platform Principals  Principal Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/profile":{"patch":{"tags":["platform-admin"],"summary":"Update Profile","description":"The person/principal profile write path (migration 0143): \"every\nuser should have a full profile ... first name, name that they go by\n... birthday ... email address, telephone number\" (Frederick,\n2026-08-09).\n\nSELF-SERVICE PLUS ADMIN, reusing the existing admin grant rather than\ninventing one: a caller may PATCH their OWN profile (`principal_id ==\nprincipal.id`) with no extra grant needed -- everyone has a profile and\neveryone must be ABLE to complete it, since Today (platform/routes.py's\nprofile_completion hook) will not stop asking otherwise -- or, holding\n'admin:people:write' (the SAME grant PEOPLE_WRITE above already\nrequires for every other principal edit), any principal's. Anyone else\nis refused with 403 before any read of the body even happens.\n\nDeliberately gated on `auth.current_principal`, not PEOPLE_WRITE: this\nroute's whole point is that a person with NO admin grant at all can\nstill fill in their own birthday, phone and emergency contact.","operationId":"update_profile_api_platform_principals__principal_id__profile_patch","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Profile Api Platform Principals  Principal Id  Profile Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/grants":{"get":{"tags":["platform-admin"],"summary":"List All Grants","description":"Every principal's live grants in one bulk read (registry.\nlist_all_active_grants) — backs the People & access per-page grant\nmatrix so the frontend doesn't fan out one request per principal.\nGated the same as the principal list itself (admin:people:read); it\nreveals nothing a caller couldn't already reconstruct by paging\nthrough every principal's own GET .../grants one at a time.","operationId":"list_all_grants_api_platform_grants_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List All Grants Api Platform Grants Get"}}}}}}},"/api/platform/watched-calendars":{"get":{"tags":["platform-admin"],"summary":"List Watched Calendars","description":"Whose calendar Rooster is watching, and when each was last swept --\nthe admin view Frederick's build task asked for (2026-09-26): \"add an\nadmin view ... listing whose calendar is watched and when each was\nlast swept. A calendar that can't be read ... raises a finding.\"\n\nGated the same as the principal list (`admin:people:read`): this is a\npeople-roster fact, not a calendar-content one -- meeting titles and\nattendees never leave `calendar_watch.watched_calendars_admin_view`'s\nown shape, only email/name/active/swept-at/error.","operationId":"list_watched_calendars_api_platform_watched_calendars_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Watched Calendars Api Platform Watched Calendars Get"}}}}}}},"/api/platform/principals/{principal_id}/grants":{"get":{"tags":["platform-admin"],"summary":"List Principal Grants","operationId":"list_principal_grants_api_platform_principals__principal_id__grants_get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Principal Grants Api Platform Principals  Principal Id  Grants Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["platform-admin"],"summary":"Create Principal Grant","operationId":"create_principal_grant_api_platform_principals__principal_id__grants_post","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGrantRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Principal Grant Api Platform Principals  Principal Id  Grants Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/grants/{function_key}":{"delete":{"tags":["platform-admin"],"summary":"Delete Principal Grant","operationId":"delete_principal_grant_api_platform_principals__principal_id__grants__function_key__delete","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}},{"name":"function_key","in":"path","required":true,"schema":{"type":"string","title":"Function Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Principal Grant Api Platform Principals  Principal Id  Grants  Function Key  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/offboard":{"post":{"tags":["platform-admin"],"summary":"Offboard Principal","operationId":"offboard_principal_api_platform_principals__principal_id__offboard_post","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Offboard Principal Api Platform Principals  Principal Id  Offboard Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/remove":{"post":{"tags":["platform-admin"],"summary":"Remove Principal","description":"Remove a principal from the app (ISS-26 for humans; extended\n2026-08-05 to agents). Frederick, 2026-08-05: \"I want to be able to\nremove users from the app. Right now I can only deactivate.\" — and,\nthe same day, tried to remove two agent principals (\"QA Temp Agent\",\ngenuine dead cruft, and \"Rooster Pipeline\", the identity the whole\nbuild/CS pipeline authenticates as) and got refused on both, because\nthe only guard was `kind != 'human'`. See registry.AgentInUse for the\nfull incident.\n\nA HUMAN's removal is unchanged: soft (registry.remove_human — the row\nstays for attribution, but the person cannot sign in, holds no access,\nand disappears from principal lists), and their open work moves\nautomatically to an active same-role successor\n(registry.find_same_role_successor, computed BEFORE removal deletes the\nrole memberships it is read from) or falls back to a manual COO card.\n\nAn AGENT's removal (registry.remove_agent) is the same soft-removal\nshape, but refuses with a 422 explaining why if the agent still holds\nlive act-capability — never bare, since a bare 403/422 here would look\nexactly like the old blanket refusal it replaces. Agents never own\nopen work directly (their bound human does), so there is no successor\nor reassignment card for this branch.","operationId":"remove_principal_api_platform_principals__principal_id__remove_post","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Principal Api Platform Principals  Principal Id  Remove Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/permission-sections":{"get":{"tags":["platform-admin"],"summary":"List Permission Sections","description":"Migration 0233's `permission_sections`, joined to their categories and\nin the product's own rank order (app/platform/permission_sections.py).\n\n⛔SERVES THE TABLE, DECIDES NOTHING. It reads no grant, resolves no\nperson and never answers \"may this caller do X\" -- it publishes the\nCATALOG of what is grantable, which is a different question from who\nholds what (`GET /effective-grants` above) and is deliberately kept a\ndifferent endpoint. Gated on admin:people:read for the same reason\n`/grants` is: it is People and access's own data.\n\nWHAT IT IS FOR. Until now the answer to \"which grant opens which page\"\nwas transcribed by hand into three separate web files, and drifted from\nthe routes twice on 2026-09-01 (ISS-395 -- see permission_sections.py's\nmodule docstring for both). This is the read that lets the screen stop\nguessing; the gate that stops the TABLE drifting from the routes is\ntests/platform/test_every_gated_route_is_grantable.py.","operationId":"list_permission_sections_api_platform_permission_sections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Permission Sections Api Platform Permission Sections Get"}}}}}}},"/api/platform/roles":{"get":{"tags":["platform-admin"],"summary":"List Roles","operationId":"list_roles_api_platform_roles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Roles Api Platform Roles Get"}}}}}},"post":{"tags":["platform-admin"],"summary":"Create Role","operationId":"create_role_api_platform_roles_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Role Api Platform Roles Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/roles/{role_id}":{"patch":{"tags":["platform-admin"],"summary":"Update Role","operationId":"update_role_api_platform_roles__role_id__patch","parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRoleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Role Api Platform Roles  Role Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["platform-admin"],"summary":"Delete Role","operationId":"delete_role_api_platform_roles__role_id__delete","parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Role Api Platform Roles  Role Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/roles/{role_id}/bindings":{"get":{"tags":["platform-admin"],"summary":"List Role Bindings","operationId":"list_role_bindings_api_platform_roles__role_id__bindings_get","parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"},"title":"Response List Role Bindings Api Platform Roles  Role Id  Bindings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["platform-admin"],"summary":"Add Role Binding","operationId":"add_role_binding_api_platform_roles__role_id__bindings_post","parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleBindingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Add Role Binding Api Platform Roles  Role Id  Bindings Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/roles/{role_id}/bindings/{function_key}":{"delete":{"tags":["platform-admin"],"summary":"Remove Role Binding","operationId":"remove_role_binding_api_platform_roles__role_id__bindings__function_key__delete","parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}},{"name":"function_key","in":"path","required":true,"schema":{"type":"string","title":"Function Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Role Binding Api Platform Roles  Role Id  Bindings  Function Key  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/roles":{"get":{"tags":["platform-admin"],"summary":"List Principal Roles","operationId":"list_principal_roles_api_platform_principals__principal_id__roles_get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Principal Roles Api Platform Principals  Principal Id  Roles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["platform-admin"],"summary":"Assign Principal Role","operationId":"assign_principal_role_api_platform_principals__principal_id__roles_post","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Assign Principal Role Api Platform Principals  Principal Id  Roles Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/roles/{role_id}":{"delete":{"tags":["platform-admin"],"summary":"Unassign Principal Role","operationId":"unassign_principal_role_api_platform_principals__principal_id__roles__role_id__delete","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}},{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unassign Principal Role Api Platform Principals  Principal Id  Roles  Role Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/principals/{principal_id}/effective-grants":{"get":{"tags":["platform-admin"],"summary":"Get Principal Effective Grants","operationId":"get_principal_effective_grants_api_platform_principals__principal_id__effective_grants_get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Principal Effective Grants Api Platform Principals  Principal Id  Effective Grants Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/effective-grants":{"get":{"tags":["platform-admin"],"summary":"List All Effective Grants","description":"Every principal's effective grants with provenance, in one bulk read\n(registry.list_all_effective_grants) — backs the People & access grant\nmatrix's From-role/Custom columns, same fan-out-free shape as the\nexisting flat GET /grants (which stays exactly as-is: the raw,\ncustom-only layer other call sites still read).","operationId":"list_all_effective_grants_api_platform_effective_grants_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List All Effective Grants Api Platform Effective Grants Get"}}}}}}},"/api/platform/constitution/{function_key}":{"patch":{"tags":["platform-admin"],"summary":"Update Constitution","operationId":"update_constitution_api_platform_constitution__function_key__patch","parameters":[{"name":"function_key","in":"path","required":true,"schema":{"type":"string","title":"Function Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConstitutionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Constitution Api Platform Constitution  Function Key  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/settings":{"get":{"tags":["platform-admin"],"summary":"List Settings","operationId":"list_settings_api_platform_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Settings Api Platform Settings Get"}}}}}}},"/api/platform/settings/{key}":{"patch":{"tags":["platform-admin"],"summary":"Update Setting","operationId":"update_setting_api_platform_settings__key__patch","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Setting Api Platform Settings  Key  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/platform/me":{"get":{"tags":["platform-admin"],"summary":"Get Me","description":"Any authenticated principal's own identity + live EFFECTIVE grant keys\n(registry.effective_grants: role-bound keys union direct/custom keys),\nso the frontend can read its own access directly instead of probing\nendpoints with throwaway requests to infer locks from 401/403 (PR #13's\nlib/access.ts). The response shape is unchanged from before roles\nexisted (`grants` is still a flat list of function_key strings) — only\nthe content is richer, so lib/access.ts's deriveSectionsFromGrants\nkeeps working unmodified and a role-bound page now unlocks for free.\n\n⛔THE CHAIRMAN GRANT IS APPENDED HERE, SYNTHETICALLY -- it is NOT a\nConstitution function key. `boardroom_grants` (migration 0394, leveled\nby 0398) is its own dedicated table by design (that migration's own\nheader explains why); this is the one seam that folds its level into\nthe SAME flat list the frontend already reads, so subNav.ts's existing\nper-item `grant` mechanism (Sidebar.tsx) can hide the four chairman nav\nitems for anyone with no active grant, without inventing a second\nfrontend gate. 'chairman:view' is present for EITHER level (write\nimplies it can read); 'chairman:write' only for write.","operationId":"get_me_api_platform_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Me Api Platform Me Get"}}}}}}},"/api/platform/view-as/colleagues":{"get":{"tags":["platform-admin"],"summary":"View As Colleagues","description":"The view-as picker's roster (Frederick, 2026-08-09: \"I can select\nthe user I want to view it as\"). Reuses\nsupervisor_service.list_assignable_colleagues -- the SAME every-active-\nhuman query the ticket-detail assignee picker already runs -- rather\nthan a new one; this is a different SURFACE for an identical need\n(\"every real, active human someone could reasonably pick\"), not a\ndifferent fact.","operationId":"view_as_colleagues_api_platform_view_as_colleagues_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response View As Colleagues Api Platform View As Colleagues Get"}}}}}}},"/api/auth/google/login":{"get":{"tags":["auth"],"summary":"Google Login","description":"Returns the Google consent-screen URL to redirect the browser to. Left\nas a JSON response (not a 302) so the frontend controls the redirect and\ncan persist `state` for the callback's CSRF check.","operationId":"google_login_api_auth_google_login_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Google Login Api Auth Google Login Get"}}}}}}},"/api/auth/google/callback":{"get":{"tags":["auth"],"summary":"Google Callback","description":"Google sends the BROWSER here, so this answers with a redirect.\n\nPorted from v1 packages/api/src/routes/auth.ts:78-219. v2 returned JSON,\nwhich meant the person completing a Google sign-in landed on a raw JSON\nblob and was never logged in -- the flow existed but could not carry\nanyone into the app.","operationId":"google_callback_api_auth_google_callback_get","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Google Callback Api Auth Google Callback Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/login-denied-flash":{"get":{"tags":["auth"],"summary":"Login Denied Flash","description":"Resolves a `denied_ref` from the login page's own URL fragment (see\n`_create_login_denied_flash`'s docstring) back to the refused email,\nEXACTLY once -- the row is deleted by the same statement that reads it,\nso a replayed or guessed token answers 404 like one that never existed.\nUnauthenticated by construction: the person reading it is, by\ndefinition, not signed in. `token` is a random opaque reference, never\nderived from the email, so it carries nothing worth protecting on its\nown.","operationId":"login_denied_flash_api_auth_login_denied_flash_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Login Denied Flash Api Auth Login Denied Flash Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/dev/login":{"post":{"tags":["auth"],"summary":"Dev Login","description":"Stub login: mints a session for a named principal by email, bypassing\nGoogle entirely. Dev/staging only - refuses outright when\nENVIRONMENT=production, live Google OAuth configured or not, because this\npath trusts a caller-supplied email with no identity proof at all.","operationId":"dev_login_api_auth_dev_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Dev Login Api Auth Dev Login Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","operationId":"logout_api_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Logout Api Auth Logout Post"}}}}}}},"/api/auth/view-as":{"post":{"tags":["auth"],"summary":"View As Start","description":"Frederick, 2026-08-09: \"log in as myself, but then view the app as\na different user, so I can see exactly what they see\" -- confirming\neach person's desk actually works for them before staff log in Monday\nmorning, especially now that ticket visibility just moved to role-based\ngrants (migrations/0137).\n\nREAD-ONLY (app/platform/auth.current_principal's own chokepoint refuses\nevery write but this route's own exit, by HTTP method -- see its\ndocstring for why per-route enumeration was rejected), STAGING ONLY\n(checked here at mint time, and again on every subsequent request --\na forged or replayed token must fail on the server, not merely be\nhidden in the UI), and AUDITED (view_as_events, migrations/0139,\nwritten BEFORE the token is minted, not after, so a session that\nsomehow never gets used is still on record as having been granted).\n\nNaturally unreachable while ALREADY viewing as someone: this route is\nitself a POST, and current_principal's read-only gate refuses every\nPOST except VIEW_AS_EXIT_PATH -- switching identities requires exiting\nfirst, so a view-as session is always exactly one identity, never\nnested.\n\n404s (not a silent no-op) on an unknown, non-human, or offboarded\ntarget -- the picker (supervisor_service.list_assignable_colleagues)\nonly ever offers a real active human, but this is the server's own\nfloor, never merely trusted from the request body.","operationId":"view_as_start_api_auth_view_as_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ViewAsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response View As Start Api Auth View As Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/view-as/exit":{"post":{"tags":["auth"],"summary":"View As Exit","description":"Restores the real session. A no-op, not an error, when called by an\nordinary (non-view-as) session -- `real_principal_id` is None there, so\nthis just re-mints that principal's own token; \"make sure I am on my\nown session\" should never itself fail because it already was.","operationId":"view_as_exit_api_auth_view_as_exit_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response View As Exit Api Auth View As Exit Post"}}}}}}},"/api/auth/stamps/connect":{"get":{"tags":["postage"],"summary":"Connect","description":"Return the hosted-login URL to send an administrator to.\n\nRETURNS THE URL RATHER THAN REDIRECTING. The caller is the System page over\nfetch(); a 307 to another origin would be swallowed by CORS and look like\nnothing happened.","operationId":"connect_api_auth_stamps_connect_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Connect Api Auth Stamps Connect Get"}}}}}}},"/api/auth/stamps/callback":{"get":{"tags":["postage"],"summary":"Callback","description":"Where Stamps.com sends the administrator back.\n\nIT REDIRECTS TO THE WEB APP, NOT TO ITSELF. The API and the web app are\ndifferent hosts on Railway (api-staging-*.up.railway.app vs\nweb-staging-*.up.railway.app), so a relative \"/system\" resolves against\nthe API and answers {\"detail\":\"Not Found\"} -- which is exactly what the\nfirst successful connection did. The OAuth flow had worked perfectly and\nthe only thing wrong was the door it opened. `frontend_url` is the same\nsetting the Google login callback already redirects through, for the same\nreason.\n\nUNAUTHENTICATED BY NECESSITY: the browser arrives here from Stamps.com,\nnot from Roost, so there is no session to require. The single-use `state`\nnonce is what makes that safe -- it was minted for one admin-initiated\nflow, it is consumed in the same statement that checks it, and a missing,\nexpired, replayed or unknown one is refused.\n\nNOTHING FROM THE QUERY STRING IS LOGGED. `code` is a credential until it\nis exchanged, and OAuth error descriptions have been known to echo it.","operationId":"callback_api_auth_stamps_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},{"name":"error_description","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Description"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/status":{"get":{"tags":["postage"],"summary":"Get Status","description":"Connection health, with no secret anywhere in it.","operationId":"get_status_api_print_postage_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Status Api Print Postage Status Get"}}}}}}},"/api/print/postage/balance":{"get":{"tags":["postage"],"summary":"Get Balance","description":"The account balance, as Stamps.com reports it, and nothing computed.\n\n⛔CORRECTED 2026-10-08 (old-shape cleanup, OLD_SHAPE_CODE_AUDIT S6). This used\nto also return `projected_need`, `pipeline_count`, `unpriced_classes`,\n`buffer` and `low` -- the balance compared against a per-parcel pipeline\nprojection plus a safety buffer, so the System panel could print \"Pipeline\nstill needing postage\" and a \"Shortfall\" line. Frederick, 2026-10-02\n(ruling `2026-10-02-the-six-day-postage-buffer.md`): \"I don't want all that\nmath. All it has to do is tell me how much postage to add and how many days\nbuffer we have in place.\" Rooster asks him that in Slack\n(`rooster/postage_buffer_ask.py`); this panel shows the balance. What the\npipeline needs, by horizon, is still answered by `/horizons` below (a\ndifferent question, ruled 2026-09-22 and kept by the 2026-10-02 ruling).","operationId":"get_balance_api_print_postage_balance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Balance Api Print Postage Balance Get"}}}}}}},"/api/print/postage/horizons":{"get":{"tags":["postage"],"summary":"Get Horizons","description":"The postage-load recommendation, by horizon -- ISSUE #2330.\n\nFrederick, 2026-09-22 (docs/01_decisions/THE_FLOOR_IS_A_PIPELINE_AND_\nPOSTAGE_IS_A_GATE.md, Gap 4): \"The model, Roost, can tell me how much\npostage I need to load every day. Or he can basically say, hey, load\nthis much postage for today, load this much for the next two days, this\nmuch for the next three days, this much for the week -- whatever. He\nshould give me an option, just so I can see how much to load up\ndepending on cash flow.\"\n\n`postage_projection.project_bulk_postage_horizons` already computed this\n(today / 2 days / 3 days / week); it had no caller anywhere in the app\nuntil this route (2026-09-26 rulings audit, issue #2330). This route\nonly recommends -- see the module's own docstring: \"IT RECOMMENDS; IT\nDOES NOT BUY.\" Adding funds stays a separate, confirmed, admin-only act\n(`post_add_funds` above).","operationId":"get_horizons_api_print_postage_horizons_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response Get Horizons Api Print Postage Horizons Get"}}}}}}},"/api/print/postage/add-funds":{"post":{"tags":["postage"],"summary":"Post Add Funds","operationId":"post_add_funds_api_print_postage_add_funds_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFundsBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Add Funds Api Print Postage Add Funds Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/labels":{"get":{"tags":["postage"],"summary":"List Labels","description":"The most recent postage, so a person can act on the one that jammed.\n\nNO IMAGE AND NO LABEL BODY, only identifiers and state. A label is\npostage; it travels to the printer, not into a browser's memory.","operationId":"list_labels_api_print_postage_labels_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Labels Api Print Postage Labels Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/boxes/{shipment_id}/ship":{"post":{"tags":["postage"],"summary":"Post Ship Box","description":"The button at the end of the packing bench: Done.\n\nFrederick, 2026-09-24 (docs/01_decisions/THE_PACKING_FLOW.md): pressing\nDone buys the Stamps.com label (Priority Mail flat rate, `postage_service.\nBOX_POSTAGE`), closes the box on its tracking number and prints the label\nat the packer's own station -- or at a named other station when this one\nhas no label printer. This is now the only route that closes a box.\n\nFrederick, 2026-08-19: \"once you close a box... you press a button to print\nout postage for that box... Since you're generating the label through\nstamps dot com, you already know the tracking number.\"\n\nONE CALL, BECAUSE IT IS ONE DECISION. Splitting it into close-then-buy\nwould let a box close and the purchase fail, leaving a shipped box with no\npostage and no obvious owner of the problem. See box_shipping for why the\nsteps run in the order they do.","operationId":"post_ship_box_api_print_postage_boxes__shipment_id__ship_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShipBoxBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Ship Box Api Print Postage Boxes  Shipment Id  Ship Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/boxes/{shipment_id}/print-label-again":{"post":{"tags":["postage"],"summary":"Post Print Box Label Again","description":"\"Print the label again\" on the Closed step (2026-09-24): the same label,\nfrom the bytes Roost kept, at this station -- never a second purchase, and\na Stamps.com reprint only when no copy is held (job_service.\nprint_box_label_again).","operationId":"post_print_box_label_again_api_print_postage_boxes__shipment_id__print_label_again_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Print Box Label Again Api Print Postage Boxes  Shipment Id  Print Label Again Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/labels/{row_id}/print-result":{"post":{"tags":["postage"],"summary":"Post Print Result","description":"Record whether the paper came out, apart from whether we paid.\n\nA jam must read as \"bought, not printed\" -- which is a reprint -- and never\nas a failed purchase, because that is the reading that invites buying the\nsame postage twice.","operationId":"post_print_result_api_print_postage_labels__row_id__print_result_post","parameters":[{"name":"row_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Row Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrintResultBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Print Result Api Print Postage Labels  Row Id  Print Result Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/manifest":{"post":{"tags":["postage"],"summary":"Post Manifest","description":"The end-of-day USPS SCAN form for today's eligible package labels.\n\nELIGIBLE ONLY. A First-Class envelope has no tracking number and no\nSCAN-form behaviour; including one makes end-of-day fail for every piece in\nthe batch, which is why the filter lives in the service rather than in the\nquery a caller happens to write.\n\n⛔LOCKED AGAINST A CONCURRENT VOID, since 2026-09-25 (state-integrity\nsweep finding F4, Fable's review of PR #2133). This used to SELECT\ncandidate rows with a plain read, call Stamps.com, then stamp\n`manifest_id` with no re-check -- so a void racing this call could\neither void a label this manifest had just told USPS was moving today,\nor (the direction this route can cause) this call could stamp\n`manifest_id` onto a label a concurrent void had just marked\n`requested`. `postage_service.void` now takes `SELECT ... FOR UPDATE`\non a label row before it ever calls the vendor; this route locks its\nown candidate rows the same way, for the span of its own vendor call,\nand the stamping UPDATE re-checks `void_status = 'none'` so a label\nvoided in the gap is never stamped.","operationId":"post_manifest_api_print_postage_manifest_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Manifest Api Print Postage Manifest Post"}}}}}}},"/api/print/postage/labels/{row_id}/reprint":{"post":{"tags":["postage"],"summary":"Post Reprint","description":"Fetch the label we already paid for.\n\nReturns whether an image came back, NOT the image: a label is postage, and\nit goes to the print pipeline rather than through a JSON response into a\nbrowser's memory and history.\n\nALSO CLOSES THIS LABEL'S OWN WASTED-POSTAGE CARD, if one is open\n(`postage_waste.resolve_card_for_label`) -- \"reprint it, if the box\nstill needs to go out\" is one of the two things that card asks for, and\nthis is the SAME route the card's own reprint action calls, never a\nsecond copy of the reprint logic (build task: \"Wire both actions to the\nexisting void and reprint paths\").","operationId":"post_reprint_api_print_postage_labels__row_id__reprint_post","parameters":[{"name":"row_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Row Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Reprint Api Print Postage Labels  Row Id  Reprint Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/postage/labels/{row_id}/void":{"post":{"tags":["postage"],"summary":"Post Void","description":"A void is a money action: `principal.id` is recorded on the label\nitself (`postage_service.void`'s own `voided_by`), and this same route\nis the one the wasted-postage card's \"cancel and refund\" action calls\n(`postage_waste.resolve_card_for_label` below) -- never a second,\nparallel void path.","operationId":"post_void_api_print_postage_labels__row_id__void_post","parameters":[{"name":"row_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Row Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Void Api Print Postage Labels  Row Id  Void Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/claim":{"post":{"tags":["print-agent"],"summary":"Claim","description":"Take the next job this machine can print, or nothing.\n\nAnswers 200 with `job: null` when there is no work, rather than 204 or an\nerror: an idle floor is the normal case, and an agent polling every few\nseconds should not have to treat \"nothing to do\" as an exception.","operationId":"claim_api_print_agent_claim_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/jobs/{job_id}/report":{"post":{"tags":["print-agent"],"summary":"Report","operationId":"report_api_print_agent_jobs__job_id__report_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Report Api Print Agent Jobs  Job Id  Report Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/awaiting-confirmation":{"post":{"tags":["print-agent"],"summary":"Awaiting Confirmation","description":"One page of the jobs this agent submitted to a press that Roost has no\nending for, with the press's own job ids\n(`agent_service.awaiting_confirmation`), and the cursor of the next page.\n\nThe agent asks at startup and every ten minutes after, paging until a page\ncomes back short, and follows each job to the press's answer, so a job\nsubmitted before a restart, or by the older agent that stopped looking after\ntwenty seconds, is not left `submitted` for ever (Frederick, 2026-10-08:\n\"You have to know when something has been successfully printed or not.\").\nA POST like every verb on this router, though it only reads.\n\nAuthenticated as a dependency (`_require_agent`), so an anonymous request\nis a 401 before anything else, like `/telemetry` and unlike the four older\nroutes that check inside their bodies.","operationId":"awaiting_confirmation_api_print_agent_awaiting_confirmation_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/AwaitingConfirmationRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AwaitingConfirmationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/still-on-the-press":{"post":{"tags":["print-agent"],"summary":"Still On The Press","description":"ONE bulk report per tracker pass: every job in `job_ids` is still on the\npress (M2 of the volume audit, 2026-10-08, Frederick: \"hundreds or even\nthousands of prints to the printer at a time\"). A single UPDATE that moves\n`updated_at` of this agent's submitted jobs among them\n(`agent_service.still_on_the_press`); it changes no status and no word, so a\njob a person settled as on the press stays settled. Answers how many it\ntouched. Authenticated as a dependency, so an anonymous request is a 401.","operationId":"still_on_the_press_api_print_agent_still_on_the_press_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StillOnThePressBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"integer"},"title":"Response Still On The Press Api Print Agent Still On The Press Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/heartbeat":{"post":{"tags":["print-agent"],"summary":"Heartbeat","description":"What the agent can see of the machine itself.\n\nFills the machine tiles the design bundle asks for and that have carried\nhonest nulls since the print wave, for want of exactly this.\n\nAnswers `attached`: the register machines this heartbeat's USB serials\nmatched, and the workstation each is now recorded on -- the receipt that\nthe wiring was re-written, readable in the agent's own log.","operationId":"heartbeat_api_print_agent_heartbeat_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Heartbeat Api Print Agent Heartbeat Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/station-assertion":{"post":{"tags":["print-agent"],"summary":"Station Assertion Route","description":"A short-lived, server-signed statement that says which workstation\nthis agent is running on -- the one `equipment.print_agent_id` links it\nto. The agent serves it on 127.0.0.1 to the Roost web page in front of\nit, and the page hands it back to `floor_blocks.clock.check_device_gate`\n(`floor_blocks.station_assertion`).\n\nFrederick, 2026-09-23: \"You already have an agent running on all of\nthem for the printing. Why can't that agent also register the\nworkstation?\" This is how it does: nobody visits a page, and the agent\ntoken that asks never leaves the agent.\n\n404 when the agent is linked to no workstation -- a print server (the\nLinux box behind the Fiery) has no desk to vouch for, and says so rather\nthan inventing one. 409 when it is linked to more than one.","operationId":"station_assertion_route_api_print_agent_station_assertion_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Station Assertion Route Api Print Agent Station Assertion Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/agent/telemetry":{"post":{"tags":["print-agent"],"summary":"Telemetry","description":"The live SNMP link (Frederick, 2026-09-30): the agent posts one\nsnapshot per change, plus a heartbeat every ten minutes -- see\n`print_agent.py`'s own polling loop. Read-only toward the printer: this\nroute never sends the agent anything to act on.\n\n⛔AUTH RUNS AS A DEPENDENCY (`_require_agent`), NOT A CALL INSIDE THIS\nBODY -- fixed 2026-09-30, main red since #2555/#2584. An anonymous POST\nused to answer 422 (missing `body`) before `_agent(conn, authorization)`\never ran, because FastAPI validates the Pydantic body in the same pass\nas an ordinary handler statement only gets to run AFTER -- see\n`_require_agent`'s own docstring. The returned `Agent` is unused here\n(same as every other print-agent route's own `_agent(...)` call site\nthat ignores the return past auth, except `claim`/`report`/`heartbeat`,\nwhich read `.name`/`.capabilities` off it) -- this route's whole\ncontract is the snapshot, never anything scoped by which agent posted\nit.\n\n404 for a host `equipment_endpoints` has no row for; 409 when more\nthan one equipment row claims it (`AmbiguousTelemetryHost` -- refusing\nrather than inventing which machine an address belongs to).\n\n⛔ATOMIC, AND ACTS ONLY AFTER COMMIT (Fable's design review of PR\n#2555, R16). `ingest_snapshot` itself does around ten writes on this\nsame connection with no transaction of its own -- wrapping the WHOLE\ncall here means a failure partway through rolls back cleanly instead\nof leaving, say, an issue filed with no matching `machine_telemetry`\nrow. The Slack DM only fires once that transaction has actually\ncommitted, using the OUTCOME the call returned, never from inside it.","operationId":"telemetry_api_print_agent_telemetry_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelemetryBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Telemetry Api Print Agent Telemetry Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/mfg/orders":{"post":{"tags":["manufacturing"],"summary":"Submit Order","description":"`SubmitOrder`. Idempotent on `centrics_order_id`.\n\n⛔A REPUSH IS NOT A SECOND ORDER. He retries on his own schedule, and a\nduplicate that created a twin could be printed twice -- which is real\nmail to a real person, twice, with no undo. Answers 200 with\n`already_existed` either way rather than a conflict, because a retry\nsucceeding is what he needs to see.\n\n⛔APPENDED 2026-10-08: a repush of an order that has never printed also\nREPLACES its recipient and sender with the repush's (his orders now carry\nthe inmate number in `recipient.attention`, consumer/api#112); the answer's\n`address` says `updated` or `unchanged`. An order that printed, has a print\njob or had an envelope scanned is never changed.\n\n⛔APPENDED 2026-10-09 (Frederick, new print templates backfilled by repush;\nOPEN_ITEMS RPA-1): the same repush also REPLACES the document of each pushed\nline that has not printed and has no live job; the answer's `documents` and\n`document_lines` say what happened to each line and why anything was\nrefused. A line that printed, is on a press or belongs to an order whose\nenvelope was scanned is never changed.\n\n⛔APPENDED 2026-10-09 (OPEN_ITEMS RPA-6; Jonathan: \"something that replaces an\norder's lines and drops the receipt\"; Frederick: \"We can only take your file so\nyou have to update your file\"): a repush that carries the order and leaves out\nits retired receipt (`receipt.pdf`, `greeting-card-receipt.jpg`) WITHDRAWS that\nline, keeping the row; `document_lines` gains an entry for it, `receipt_withdrawn`\nor a refusal. A photo receipt is never dropped; no other line is ever removed.\n\n⛔APPENDED 2026-10-11 (OPEN_ITEMS ROD-1; Frederick: \"the backlog is addressed\nfirst, and then we catch up\"): the push may carry `retailer_order_date`, when\nthe order was placed (RFC 3339 with an offset; a time with no offset is a 422).\nA new order stores it. A repush of an order with NO stored date fills it; a\nrepush whose date differs from the stored one changes nothing and says so on\nthe order's trail and in the answer's `order_date` (`differs`). The ready list\nand the floor work the oldest order first by it (`print_order.order_clock_sql`).\n\n⛔APPENDED 2026-10-11 (the lead; `print.order_duplicates`, FLR-25 History): a NEW\norder whose lines are the same document for the same parcel code as an order\nRoost already holds (the signed link may differ) is recorded as a DUPLICATE and\nnever printed. It is still a 200 with `already_existed` true, the original's\n`OrderStatus`, `duplicate` true and `duplicate_of` naming the original: a retry\nof a parcel under a new order id is received, not refused, and nothing is lost.","operationId":"submit_order_api_mfg_orders_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitOrderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Submit Order Api Mfg Orders Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/mfg/orders/{centrics_order_id}/status":{"get":{"tags":["manufacturing"],"summary":"Query Order Status","description":"`QueryOrderStatus`. Answers with Centric's own word under\n`OrderStatus`, which is the field his client already reads out of\n`MfgOrderStatusList.MfgOrderStatus[0]`.","operationId":"query_order_status_api_mfg_orders__centrics_order_id__status_get","parameters":[{"name":"centrics_order_id","in":"path","required":true,"schema":{"type":"string","title":"Centrics Order Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Query Order Status Api Mfg Orders  Centrics Order Id  Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking/company-meetings":{"get":{"tags":["booking"],"summary":"List Company Meetings","description":"What the team said in its own meetings.\n\nFrederick, 2026-08-26: \"The team meeting should just be ... go into a\nknowledge base for rooster to understand what the team is discussing ...\nhas nothing to do with the York County call.\"\n\n⛔AUTHENTICATED, AND DELIBERATELY NOT PUBLIC. These are internal\nconversations -- pricing, staffing, what is going wrong -- and the only\nreason they are reachable at all is that somebody inside the company, or\nRooster on their behalf, needs to know what the team is working on.\n\nMeetings that DID match an account are absent by construction: those\nbelong to that account's own record, sealed to it. See\n`company_meetings.recent`.","operationId":"list_company_meetings_api_booking_company_meetings_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Company Meetings Api Booking Company Meetings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking/team-meeting/agenda":{"get":{"tags":["booking"],"summary":"Team Meeting Agenda","description":"Build task Unit D, point 4: \"THE RECURRING MEETING IS THE SETTLER\" --\nlast Wednesday sync's unfinished business, with everything Roost could\nsettle on its own already closed before anyone reads this.\n\nSame authentication posture as `/company-meetings` right above: internal\nconversation, not public, but no narrower grant than \"is a principal at\nall\" -- this is the room's own agenda, not a restricted desk.","operationId":"team_meeting_agenda_api_booking_team_meeting_agenda_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Team Meeting Agenda Api Booking Team Meeting Agenda Get"}}}}}}},"/api/booking/missed-next-steps":{"get":{"tags":["booking"],"summary":"Missed Next Steps","description":"The manager-facing missed-next-steps list (GitHub issue #2319,\nFrederick 2026-09-14, R-154; PLAN-ACTIVE-TODAY-CARDS-SETTLE-001.md §F).\n\nScoped to the CALLER: their own direct reports' missed attestable next\nsteps, plus any that never got a confident name at all (visible only if\nthe caller holds the founder pool's own grant) -- see\n`meeting_next_steps_service.missed_next_steps_for`'s own docstring for\nthe exact rule. Same authentication posture as `/team-meeting/agenda`\nabove (any authenticated principal may call this; the SCOPING, not a\nroute-level grant, is what keeps a manager from seeing another\nmanager's reports) -- a manager with no reports and no founder-pool\ngrant gets back an empty, honest list, never a 403.","operationId":"missed_next_steps_api_booking_missed_next_steps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Missed Next Steps Api Booking Missed Next Steps Get"}}}}}}},"/api/booking/availability/{principal_id}":{"get":{"tags":["booking"],"summary":"Read Availability","description":"Their window, or the unsaved default. Never 404 -- see service.get_window.\n\nBuilt by `_window_payload` rather than assembled here. The two used to be\nidentical copies, which is how a field lands on one route and not the\nother: `saved` was added below and this GET -- the one the page actually\nreads on load -- would have kept answering without it.","operationId":"read_availability_api_booking_availability__principal_id__get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Availability Api Booking Availability  Principal Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["booking"],"summary":"Write Availability","operationId":"write_availability_api_booking_availability__principal_id__put","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Write Availability Api Booking Availability  Principal Id  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking/availability/{principal_id}/attendance":{"put":{"tags":["booking"],"summary":"Write Attendance","description":"Attach somebody to every demo, or stop attaching them.\n\nADMIN ONLY, EVEN FOR YOUR OWN ROW -- the one way this differs from the\nwindow beside it. `_may_edit` lets a person edit their own hours because\ntheir hours are theirs; \"I am on every sales call\" is a claim about\neverybody else's calls, and nobody gets to make that one about themselves.\n\nFrederick, 2026-08-21, asking whether this scales: \"And will we add future\nsalespeople?\" It does now. A new salesperson needs three things and all\nthree have a surface: the `demos:accept` grant (admin), their own hours\nwith accepting on (their profile), and the VP flagged HERE so the new\nperson is not booked alone. Before this the third had no write path\nanywhere and Keith's was set with a hand-run UPDATE.","operationId":"write_attendance_api_booking_availability__principal_id__attendance_put","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttendanceBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Write Attendance Api Booking Availability  Principal Id  Attendance Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking/upcoming":{"get":{"tags":["booking"],"summary":"Upcoming Meetings","description":"Every demo still ahead, for the Today page.\n\nFrederick, 2026-08-21: \"whenever there's an upcoming meeting, it can always\nbe visible on the today page, and then that's where the cancel meeting can\nbe ... one place so you don't have to go into the lead board and do stuff\nin the lead board.\"\n\nALWAYS VISIBLE, WHICH IS WHY IT IS NOT AN ACTION ITEM. A booking already\nproduces a `demo_request_booked` card, and a card is something a person\nclears -- resolve it and the meeting stops being visible while remaining\nentirely real. A meeting that is still coming is a STATE, not a request,\nand it belongs on the page for as long as it is true. Today is where\nRooster pulls a person in, so it is the one place this has to be.\n\nNOT SCOPED TO THE HOST. `sales:read` sees every upcoming meeting, not only\ntheir own: a demo landing on a colleague's calendar while they are on a\nplane is exactly when somebody else needs to see it, and a spam booking is\nnobody's. Frederick, on who cancels: \"Keith should have it. I should have\nthat ability. Admin should have it. Sales manager should have it. A\nsalesperson should have it.\"\n\nCancelled bookings are excluded -- that is what keeping the row rather than\ndeleting it buys: \"booked then cancelled\" stays a fact without the meeting\npretending to still be coming.","operationId":"upcoming_meetings_api_booking_upcoming_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Upcoming Meetings Api Booking Upcoming Get"}}}}}}},"/api/booking/my-upcoming":{"get":{"tags":["booking"],"summary":"My Upcoming Meetings","description":"The meetings THIS person is actually in, for their own Today page.\n\n⛔A DIFFERENT QUESTION FROM `/upcoming`, WHICH IS WHY IT IS A DIFFERENT\nROUTE. That one answers \"every meeting the company has ahead\", gated on\nsales grants, because seeing it is what makes cancelling possible. This\none answers \"what have I got coming up\", and the answer is nobody else's\nbusiness -- so it needs no grant at all, which is the point: most of the\ncompany holds no sales grant and had no way to see their own calendar.\n\nFrederick, 2026-09-04: \"the meeting that I should see coming up is\nmeetings that you participate in as an employee. So if Cindy's not on a\nsales meeting, invite, or booking ... then there's no reason for Cindy to\nhave that meeting on her calendar. We're showing her a meeting that she\ncan't join. But if she was added on a booking invite by Locke, then she\nshould see it.\"\n\nONE SHAPING PATH. Both routes run `_upcoming`, so a card cannot come out\nof one looking different from the other -- the same reason\n`calendar_watch.upcoming_observed` is one query with a parameter rather\nthan two that drift.","operationId":"my_upcoming_meetings_api_booking_my_upcoming_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Upcoming Meetings Api Booking My Upcoming Get"}}}}}}},"/api/booking/locke-bookings/{booking_id}/cancel":{"post":{"tags":["booking"],"summary":"Cancel Locke Booking","description":"Cancel a meeting Locke booked, and undo what the booking did.\n\nFrederick, 2026-08-21: \"we need the cancel button on the today tab to\ncancel the meeting because we don't have any way to cancel Locke meetings\n... we won't have a way to remove it from our leads and all that.\"\n\nSAME GRANT AS THE DEMO CANCEL, deliberately. The people who may kill a\nmeeting are the same people whichever page booked it, and a second grant\nwould have to be kept in step with the first by hand.\n\nThe response reports each step separately for the reason the demo one does:\na vendor may refuse without vetoing the cancellation, and an invite still\nsitting in somebody's calendar is something a person has to be told rather\nthan have summarised as \"cancelled\".","operationId":"cancel_locke_booking_api_booking_locke_bookings__booking_id__cancel_post","parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Booking Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelDemoRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel Locke Booking Api Booking Locke Bookings  Booking Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking/demo-requests/{demo_request_id}/cancel":{"post":{"tags":["booking"],"summary":"Cancel Demo Request","description":"Cancel a booked demo and undo the booking.\n\n`booking:cancel_meeting`, NOT `sales:write`. Frederick, 2026-08-21:\n\"meetings can only be canceled on my or the Sales, sales manager's Today\ntab.\"\n\nThis route used to reuse `sales:write` on the argument that it was already\n\"exactly who\" should hold it. What unmakes that is that `sales:write` does\nnot MEAN this: it is CRM access, held by an account manager and by a VP of\nGovernment Sales for unrelated reasons, so gating a meeting on it hands\ncancellation to whoever was given the CRM for some other purpose.\n\n(An earlier version of this docstring said Keith held no `sales:write` at\nall. That was read off `function_grants` alone and was wrong -- his Sales\nManager role carries it. Permissions arrive by two independent paths here,\nand reading one of them gives a confident wrong answer. Migration 0231\nkeeps the correction.)\n\nMigration 0231 seeds the new key to Frederick, the Sales Manager role, and\nwhoever holds `sales:owns_leads`.\n\nNOT gated on owning the booking. A spam booking is nobody's, and a demo\nlanding on a salesperson's calendar while they are on a plane is precisely\nwhen a colleague needs to be able to kill it.\n\nThe response reports each step separately rather than a single \"ok\",\nbecause a vendor is allowed to refuse without vetoing the cancellation --\nsee cancellation.py. `calendar_cancelled: false` means the invite may still\nbe in somebody's calendar and a person needs to know that, not be told the\nword \"cancelled\" and left to find out.","operationId":"cancel_demo_request_api_booking_demo_requests__demo_request_id__cancel_post","parameters":[{"name":"demo_request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Demo Request Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelDemoRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel Demo Request Api Booking Demo Requests  Demo Request Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking/huddle":{"post":{"tags":["booking"],"summary":"Book Huddle","description":"Get two people onto a live sync to unblock one of them, right now.\n\n⛔ROOSTER-ONLY, spelled `rooster:` per this codebase's own naming\nconvention (ci/route_callers.py's own docstring) -- this is a CEO\nexecution action (the build task's own architecture: a fourth model\nseat that EXECUTES what the board decided), not a human clicking a\nbutton on the Today page. Unit H's own build task builds the window a\nperson sets their own hours in and the mechanism that books a huddle\nagainst those hours; deciding WHICH work items warrant a huddle at all\nis left for a later unit to wire -- see this module's own build report.\n\nNo `_may_edit` here on purpose, unlike `/availability/{id}`: this route\nnever writes anybody's OWN hours, it only READS both people's already-\nsaved (or defaulted) windows to find a shared opening -- the same\ncompany rule migration 0388's own header states: \"if your calendar has\navailable time, it can be booked during business hours... The absence\nof a block IS the consent.\"","operationId":"book_huddle_api_booking_huddle_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookHuddleRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Book Huddle Api Booking Huddle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/orgs/suggest":{"get":{"tags":["public-demo-request"],"summary":"Suggest Organizations","description":"Public-safe autocomplete over the organizations SSOT (2,727 rows).\n\nReturns name/facility_type/state and NOTHING ELSE:\n  * NO id, NO lifecycle_stage, NO \"matched\" signal of any kind -- section\n    1's own rule for the full form (\"The visitor must never be told\n    whether they matched an existing record\") applies here first, since\n    this is the endpoint that would leak it.\n  * NO adp -- Frederick, 2026-08-20, overruling the handoff spec's own\n    §4: \"We don't want to show our ADP to people or put our ADP in for\n    this form.\"\n\n  * CORRECTIONAL FACILITIES ONLY, which is the sharpest of the three.\n\nTHIS USED TO SEARCH EVERY ORGANIZATION REGARDLESS OF TYPE, on the\nreasoning that a law firm on file matches a name exactly like a facility\ndoes. That reasoning was about matching and ignored what the endpoint IS:\nan unauthenticated name oracle. Typing two letters returned \"The Law\nOffices of Erik Babcock\" and \"Law Office of Nikolaus W Reed\" to anybody on\nthe internet.\n\nFrederick, 2026-08-21: \"You never show any other agency or else people can\nuse this to figure out who's in our CRM. You only show public correctional\nfacilities. That's it.\"\n\nThe line is public record, not sensitivity. A county jail exists whether or\nnot it has ever heard of us -- naming one back reveals nothing. A law firm,\na vendor, an advocacy group in `organizations` is there BECAUSE of a\nrelationship with us, so returning its name discloses the relationship. An\nattorney can still book: they type their firm, it matches nothing, and\n\"Use ... as a new facility\" takes it (see `offerToAddTyped`). They simply\ncannot enumerate who else we work with on the way past.","operationId":"suggest_organizations_api_public_orgs_suggest_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Suggest Organizations Api Public Orgs Suggest Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/demo/config":{"get":{"tags":["public-demo-request"],"summary":"Demo Config","description":"The public settings the demo page cannot know until it asks.\n\nToday that is one field: the Turnstile SITE KEY, which is public by\ndefinition (Cloudflare reads it out of the rendered page) and returned\nhere rather than baked into the web bundle.\n\nWHY AN ENDPOINT AT ALL, FOR A VALUE THAT NEVER CHANGES. It was a Vite\nbuild-time variable, `VITE_TURNSTILE_SITE_KEY`. It was set on the Railway\nweb service, it was correct, and it never reached the bundle -- because\napps/web/Dockerfile declares no matching `ARG`, so `npm run build` inside\nthe image never saw it. Nothing failed. The dashboard said configured,\nCI stayed green (the web tests supply their own key), and the deployed\npage simply rendered no widget.\n\nThat is worse than it sounds, because the SERVER half was configured\ncorrectly: `POST /demo/request` had a secret, so it required a token,\nand the page had no way to produce one. Every real visitor would have\nbeen answered 403 \"We could not verify you are human\" on the last click\nof a form they had already filled in. The first person to find that would\nhave been a warden.\n\nSo the page asks the API, and the API answers from the same credential\nstore that decides whether the check runs at all\n(`turnstile.load_site_key` / `load_secret`). One fact, one place: they\ncannot disagree, and rotating the pair no longer needs a web rebuild.\n\nRate-limited on the availability budget rather than its own: it is one\ncheap read, fired once per page load, on the same visit that is about to\nfetch a grid.","operationId":"demo_config_api_public_demo_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Demo Config Api Public Demo Config Get"}}}}}}},"/api/public/demo/availability":{"get":{"tags":["public-demo-request"],"summary":"Demo Availability","description":"Pooled, live, UTC half-hours across every accepting host.\n\nHOST-SIDE, NOT VISITOR-SIDE. `tz` is validated here -- a malformed zone\nis a 422 the page can show next to the field before ever drawing a grid\nit cannot render dates for -- but it does not change WHICH slots exist.\navailability.py's own module docstring explains why: each host's window\nis read in THEIR zone, because that is what keeps 9am 9am to them\nthrough a DST switch. The visitor's zone is a display concern the page\napplies to these same UTC instants after the fact.\n\nNEVER NAMES A HOST. Every accepting member's slots are pooled into one\nundivided set of UTC instants (deduplicated on start/end); nothing in\nthis response is keyed by, or traceable to, which principal offered\nwhich slot. See this module's own docstring for Frederick's quote.\n\nA CONSTRAINING ATTACHED VP'S CALENDAR IS SUBTRACTED HERE TOO\n(`_constraining_busy`), not only at booking time -- otherwise this grid\ncould offer a half-hour the booking re-check would then correctly\nrefuse, landing the visitor on an unexplained 409.\n\nAn empty roster (nobody holds `demos:accept` and has `accepting=true`,\nor none of them could be verified live) answers `{\"slots\": []}` --\nan honest empty grid, never an error, matching\n`availability.slots_for`'s own \"unfinished profile offers nothing\"\ncontract.","operationId":"demo_availability_api_public_demo_availability_get","parameters":[{"name":"tz","in":"query","required":true,"schema":{"type":"string","title":"Tz"}},{"name":"turnstile_token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Demo Availability Api Public Demo Availability Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/demo/request":{"post":{"tags":["public-demo-request"],"summary":"Request Demo","description":"Books a real half-hour with whichever accepting host it is fairest to\nhand it to, or answers 409 `{\"error\": \"slot_taken\"}` when the visitor's\nchosen slot is gone.\n\nORDER OF CHECKS MATTERS. The honeypot runs FIRST and short-circuits\neverything else, including Turnstile and even the elapsed-time check: a\nscript that fills every field it finds -- including the hidden one --\nis answered as cheaply as possible, with a plausible fake success and\nnothing written, before a single database query or Turnstile call is\nspent on it. Real defenses (elapsed-time, then Turnstile) run only for\nsubmissions that got past the trap; the rate limiter runs before all of\nit, since it is the cheapest check and the one that protects the other\ntwo from being run at all under a flood.","operationId":"request_demo_api_public_demo_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoRequestBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Request Demo Api Public Demo Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/demo/visit":{"post":{"tags":["public-demo-request"],"summary":"Demo Visit","description":"Records one view of the public demo page into `web_analytics_events`,\nthe visits `demo_requests` is the conversions of. No Turnstile: the page\nhas not rendered a widget yet when it loads, and a row here books\nnothing. The rate limit is the bound.","operationId":"demo_visit_api_public_demo_visit_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoVisitBody"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/document-templates":{"get":{"tags":["org_documents"],"summary":"List Document Templates","description":"Every published version of one kind, newest first -- the live one\nmarked -- plus the placeholder catalog the editor offers.","operationId":"list_document_templates_api_sales_document_templates_get","parameters":[{"name":"kind","in":"query","required":true,"schema":{"type":"string","title":"Kind"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response List Document Templates Api Sales Document Templates Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["org_documents"],"summary":"Publish Document Template","description":"Publish the draft as the kind's next version and make it live. A\nWord template is the uploaded .docx, or (`keep_live_source`) the live\nversion's own file with new questions or choices.","operationId":"publish_document_template_api_sales_document_templates_post","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_publish_document_template_api_sales_document_templates_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Publish Document Template Api Sales Document Templates Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/document-templates/{template_id}/source":{"get":{"tags":["org_documents"],"summary":"Document Template Source","description":"A Word template's own file, exactly as published.","operationId":"document_template_source_api_sales_document_templates__template_id__source_get","parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/document-templates/preview":{"post":{"tags":["org_documents"],"summary":"Preview Document Template","description":"The unsaved draft (Markdown text, or an uploaded / the live .docx),\nfilled against a real organization for the choices given. Stores\nnothing and makes no PDF.","operationId":"preview_document_template_api_sales_document_templates_preview_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_preview_document_template_api_sales_document_templates_preview_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Preview Document Template Api Sales Document Templates Preview Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/document-templates/generator-settings":{"get":{"tags":["org_documents"],"summary":"Get Generator Settings","description":"The proposal generator's own settings card (design's \"Generator\nsettings\", `Org Documents.dc.html` L325-336): Upfront below, Name on\nnotices clause, Support email, Keep originals. Backed by the\npre-existing `PROPOSAL_GENERATOR` setting\n(`settings_service.proposal_generator`, already read at generation,\nfollow-up and stale-sweep time, migration <= 0610) -- this route and\nthe PUT below are its first reader/writer OUTSIDE those internal call\nsites, i.e. its first screen (2026-10-02, S4 \"Org record + templates\"\nbuild).","operationId":"get_generator_settings_api_sales_document_templates_generator_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Generator Settings Api Sales Document Templates Generator Settings Get"}}}}}},"put":{"tags":["org_documents"],"summary":"Set Generator Settings","operationId":"set_generator_settings_api_sales_document_templates_generator_settings_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeneratorSettingsIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Set Generator Settings Api Sales Document Templates Generator Settings Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}/documents":{"get":{"tags":["org_documents"],"summary":"List Org Documents","description":"Every proposal and contract generated for this organization, newest\nfirst: each version's date, template version, rep, status and final\nprice, and every send's delivery status.","operationId":"list_org_documents_api_orgs_organizations__org_id__documents_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response List Org Documents Api Orgs Organizations  Org Id  Documents Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["org_documents"],"summary":"Generate Org Document","description":"Generate from the live template and save it on the organization as\na new version (earlier versions are kept).","operationId":"generate_org_document_api_orgs_organizations__org_id__documents_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Generate Org Document Api Orgs Organizations  Org Id  Documents Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}/documents/form":{"get":{"tags":["org_documents"],"summary":"Org Document Form","description":"The generate form: what is filled in, what to ask, and the pricing\npanel, for the choices and deal inputs so far (`picked` and `deal` are\nJSON, as the form holds them).","operationId":"org_document_form_api_orgs_organizations__org_id__documents_form_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}},{"name":"kind","in":"query","required":true,"schema":{"type":"string","title":"Kind"}},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contact Id"}},{"name":"from_document_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"From Document Id"}},{"name":"picked","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Picked"}},{"name":"deal","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Deal"}},{"name":"adp","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Adp"}},{"name":"rep_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Rep Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Org Document Form Api Orgs Organizations  Org Id  Documents Form Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/documents/{document_id}/status":{"post":{"tags":["org_documents"],"summary":"Set Org Document Status","description":"Mark a proposal signed or declined (spec §11's status, widened from\n§7's original four).","operationId":"set_org_document_status_api_orgs_documents__document_id__status_post","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Set Org Document Status Api Orgs Documents  Document Id  Status Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/documents/{document_id}/cancel":{"post":{"tags":["org_documents"],"summary":"Cancel Org Document","description":"Cancel a draft proposal nobody has sent or signed yet (Frederick,\n2026-09-30: \"if a proposal stays in draft form and never gets signed or\nsent ... I should be able to cancel it or delete it or remove it\").\n`orgs:write` -- the same grant that can generate one. Refused (409) once\nthe proposal has ever left `draft`: \"any proposal that we've sent or get\nsigned ... becomes part of a record\".","operationId":"cancel_org_document_api_orgs_documents__document_id__cancel_post","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Cancel Org Document Api Orgs Documents  Document Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/documents/{document_id}/void":{"post":{"tags":["org_documents"],"summary":"Void Org Document","description":"Void a sent proposal with no decision yet (Frederick, 2026-10-01:\n\"I should be able to void proposals because I can send a proposal.\nThey might suggest an edit, go back, void it, edit, send a new one.\").\n`orgs:write` -- the same grant that can cancel a draft or mark a\nproposal signed/declined. Refused (409) once the proposal is `signed`\n(\"a proposal that's hasn't been sent yet, it should be able to be\ndeleted. Just like invoices\" -- a signed proposal is the customer's own\nexecuted record).","operationId":"void_org_document_api_orgs_documents__document_id__void_post","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__org_documents__routes__VoidIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Void Org Document Api Orgs Documents  Document Id  Void Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/documents/{document_id}/follow-up":{"post":{"tags":["org_documents"],"summary":"Log Org Document Follow Up","description":"Log a follow-up check-in on an outstanding (sent) proposal\n(Frederick, 2026-10-01: \"that's something should be done by the\nhuman\"). Closes any open follow-up task for it and resets the clock\nboth the follow-up schedule and the stale rule run from.","operationId":"log_org_document_follow_up_api_orgs_documents__document_id__follow_up_post","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowUpIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Log Org Document Follow Up Api Orgs Documents  Document Id  Follow Up Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/proposals/outstanding":{"get":{"tags":["org_documents"],"summary":"List Outstanding Proposals","description":"Every `sent` proposal with no decision yet, oldest first (spec §11's\nfollow-up list) -- read-only, for the sales/growth reporting screen and\nfor Rooster's own sales context.","operationId":"list_outstanding_proposals_api_sales_proposals_outstanding_get","parameters":[{"name":"rep_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Rep Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response List Outstanding Proposals Api Sales Proposals Outstanding Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/proposals/metrics":{"get":{"tags":["org_documents"],"summary":"Proposal Metrics Route","description":"Spec §11's metrics: outstanding count/value, aging, win rate, days to\nsign, value by status and rep, and the follow-up list -- feeding the\nexisting leads/deals reporting.","operationId":"proposal_metrics_route_api_sales_proposals_metrics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Proposal Metrics Route Api Sales Proposals Metrics Get"}}}}}}},"/api/orgs/documents/{document_id}/pdf":{"get":{"tags":["org_documents"],"summary":"Org Document Pdf","description":"The exact PDF that was generated -- the stored bytes, never a fresh\nrender. Anyone who can see the organization may download it.\n\nFrederick, 2026-10-01: \"we're going to manually send proposals out.\nIt's not an automated thing. So when you download a proposal, we\nreally should just mark it as sent.\" Downloading a draft marks it\n`sent` (`service.mark_sent_on_download`) -- a no-op on anything already\npast `draft`, so re-downloading a sent proposal changes nothing.","operationId":"org_document_pdf_api_orgs_documents__document_id__pdf_get","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/documents/{document_id}/docx":{"get":{"tags":["org_documents"],"summary":"Org Document Docx","description":"The same frozen words as a Word document, for a customer's counsel to\nredline. Marks a draft `sent`, the same as the PDF download above --\nsee `service.mark_sent_on_download`.","operationId":"org_document_docx_api_orgs_documents__document_id__docx_get","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/documents/{document_id}/send":{"post":{"tags":["org_documents"],"summary":"Send Org Document","description":"Send the PDF through the composer, from the sender's own address.\nThe answer is what the send ledger recorded for THIS attempt.","operationId":"send_org_document_api_orgs_documents__document_id__send_post","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Document Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Send Org Document Api Orgs Documents  Document Id  Send Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/parcel-request/facilities":{"get":{"tags":["public-return-mail"],"summary":"Parcel Request Facilities","description":"The facilities we ship to, searched by name or by the code on the envelope.\n\nFrederick, 2026-09-01: \"letting it be an autocomplete only to the facilities\nthat we have active contracts and shipping to. That way, it's guaranteed to\nresolve ... We should also accept the facility code.\"\n\n⛔IT ANSWERS A LOOKUP, IT DOES NOT PUBLISH A CUSTOMER LIST. Three\ncharacters minimum for a name, matched at a word boundary, ten rows\nmaximum, rate limited, and no empty-query listing. The roster is **44\nfacilities across 14 organizations** (measured on production 2026-09-01),\nand every name and code on it is printed on the envelopes these senders\nalready addressed -- so this is not a secret. It is also not a download.\n\n⛔AND THE LIMITER IS A SPEED BUMP, NOT A BOUND. `LOOKUP_RATE` is per-IP: it\nstops one curious person and does nothing at all about somebody rotating\naddresses. What makes this safe to expose is the VALUE of the data, not the\ndifficulty of collecting it -- no ADP, no contact, no contract, no org id,\nand nothing here that a competitor could not also get from procurement\nrecords. Do not let \"rate limited\" carry more weight than that.\n\n⛔AND IT RETURNS THE NAME FOR A CODE. That is the whole point of accepting\nfour digits: the sender types what they addressed the parcel to and the page\nshows them the facility it resolves to, so a mistyped digit is visible to\nthe person who can tell it is wrong -- before it decides which account\nmanager gets the ticket.","operationId":"parcel_request_facilities_api_public_parcel_request_facilities_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Parcel Request Facilities Api Public Parcel Request Facilities Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/parcel-request/config":{"get":{"tags":["public-return-mail"],"summary":"Parcel Request Config","description":"What the page needs to draw itself: the exact waiver text and the hold\nwindow.\n\n⛔THE TEXT COMES FROM THE SERVER, NOT THE PAGE. If the page carried its own\ncopy, the words somebody signed would be whatever bundle their browser had\ncached — and we would be storing a signature against text we cannot prove\nthey saw.","operationId":"parcel_request_config_api_public_parcel_request_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Parcel Request Config Api Public Parcel Request Config Get"}}}}}}},"/api/public/parcel-request":{"post":{"tags":["public-return-mail"],"summary":"Request Return Mail","description":"Take the request, take the signature, open the ticket, raise the job.\n\nAnswers with the reference the customer quotes back to us. Deliberately\ndoes NOT answer with the ticket id or the signature id: those are ours,\nand a public endpoint that hands out internal identifiers invites somebody\nto try them.","operationId":"request_return_mail_api_public_parcel_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReturnMailBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Request Return Mail Api Public Parcel Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/queue":{"get":{"tags":["return-mail"],"summary":"List Open","description":"What is owed, soonest deadline first: exactly the jobs a Today card\nhas been raised for (`service.open_jobs`).\n\n⛔NOT \"ONLY THE SIGNED PATH\", as this used to say. A prepaid-label request\nbecomes a job the moment its label is confirmed and its parcel found, and\nits card sends the technician here to record the day it left.","operationId":"list_open_api_returns_queue_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Open Api Returns Queue Get"}}}}}}},"/api/returns/by-ticket/{ticket_id}":{"get":{"tags":["return-mail"],"summary":"Get By Ticket","description":"The return-mail request behind a ticket, if there is one.\n\n⛔THE TICKET IS WHERE THE WORK IS. Frederick's design routes this whole\nprocess through one open thread: the form opens the ticket, the signed PDF\nis its first message, and \"all the updates now are just appended to that\nsame ticket until the process is completely closed.\" So the place a person\nresolves the parcel is the ticket they are already reading, not a queue\nsomewhere else that they would have to know exists.\n\n⛔AND IT ANSWERS `null` RATHER THAN 404. Every b2c ticket asks this\nquestion and almost none of them are return-mail; a 404 per ticket view\nwould make the ordinary case look like a broken panel in the console.","operationId":"get_by_ticket_api_returns_by_ticket__ticket_id__get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get By Ticket Api Returns By Ticket  Ticket Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/{request_id}":{"get":{"tags":["return-mail"],"summary":"Get One","operationId":"get_one_api_returns__request_id__get","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get One Api Returns  Request Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/{request_id}/claim":{"post":{"tags":["return-mail"],"summary":"Claim","description":"I have this one. Refuses if somebody else already does, and says who.","operationId":"claim_api_returns__request_id__claim_post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Claim Api Returns  Request Id  Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/box-for/{parcel_code}":{"get":{"tags":["return-mail"],"summary":"Box For","description":"Turn a parcel code into the box its original mail is sitting in.\n\nFrederick, 2026-08-27: \"when I type in a parcel ID, it's supposed to fetch\nthe box number. And then continue to next step.\"\n\n⛔A CUSTOMER SERVICE AGENT CANNOT LOOK THIS UP THEMSELVES. That is the whole\nreason this endpoint exists. The panel used to ask them to type a box number\nand told them it was \"on the parcel record\" -- it is not on anything they\ncan open. The number lives in the mail service's `scans.box_label_id` and\nnowhere else.\n\n⛔AND IT NEVER RAISES. Every outcome is an answer with a reason, because\nthe agent is mid-task with a customer waiting: a code that does not exist,\na parcel that has not been scanned yet, and a mail service we cannot reach\nare three different situations and only one of them is their mistake.","operationId":"box_for_api_returns_box_for__parcel_code__get","parameters":[{"name":"parcel_code","in":"path","required":true,"schema":{"type":"string","title":"Parcel Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoxAnswer"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/{request_id}/parcel":{"post":{"tags":["return-mail"],"summary":"Record Parcel","description":"Customer service has found the parcel. This is what creates the job.\n\nFrederick, 2026-08-26: \"This process is a customer service human process\n... The human is gonna go and resolve the box number themselves. Once they\nresolve the box number, then that creates the task for the fulfillment\ntechnician to pull the parcels and send it back.\"\n\n⛔GATED ON CUSTOMER SERVICE, NOT FULFILMENT. Finding which of a sender's\nparcels this is means reading their request against their mail history --\ndesk work, and the escalation card is already on a CS queue. A technician\nis holding a box, not a browser.","operationId":"record_parcel_api_returns__request_id__parcel_post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Parcel Api Returns  Request Id  Parcel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/{request_id}/label-received":{"post":{"tags":["return-mail"],"summary":"Label Received","description":"Customer service confirms the prepaid label arrived and is usable.\n\n⛔GATED ON CUSTOMER SERVICE, NOT FULFILMENT, because judging a label is\ndesk work: the agreement requires a valid, fully paid USPS label addressed\nto the parcel's OWN return address, and whoever is reading the customer's\nemail is the person who can see whether the PDF that arrived is that.\n\nThis is the call that turns option 2 into a job. Before it, a technician\nhas nothing to do -- and a card raised at submission would sit on their\nToday list waiting on a stranger's inbox.","operationId":"label_received_api_returns__request_id__label_received_post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Label Received Api Returns  Request Id  Label Received Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/{request_id}/cancel":{"post":{"tags":["return-mail"],"summary":"Cancel","description":"RULING 2026-09-29: the other half of `UnfinishedReturnRequestBlocksTransition`'s\nrefusal message, \"finish or cancel the return first\" -- see\n`service.cancel`'s own docstring for why this route did not exist\nbefore today, though the column it writes has existed since the\ntable's first migration.\n\n⛔GATED ON CUSTOMER SERVICE, NOT FULFILMENT, the same reasoning\n`record_parcel`/`label_received` above already carry: deciding a\nrequest will never be finished is desk work, reading the customer's own\nwords against the ticket, not a technician's call.","operationId":"cancel_api_returns__request_id__cancel_post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel Api Returns  Request Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/returns/{request_id}/complete":{"post":{"tags":["return-mail"],"summary":"Complete","description":"It went back to the post office. Settles the Today card and drafts the\ncustomer's reply -- a DRAFT, never a send: this is the last word a customer\nhears about their own property.","operationId":"complete_api_returns__request_id__complete_post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Complete Api Returns  Request Id  Complete Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/status":{"get":{"tags":["consumer-accounts"],"summary":"Connection Status","description":"Is Roost connected to Pigeonly's customer accounts, and what is it still\nwaiting on? Read by whoever is about to look a customer up, so \"not\nconnected\" is a label and not a surprise.\n\nLive, it asks Pigeonly who Roost is (`GET /admin/me`): `acting_as` is the\nservice user's name, roles and permissions, and a refused key or a\nmissing role shows up here as `problem` rather than on the first search.","operationId":"connection_status_api_consumer_accounts_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Connection Status Api Consumer Accounts Status Get"}}}}}}},"/api/consumer-accounts/search":{"get":{"tags":["consumer-accounts"],"summary":"Search Customers","description":"Find a customer by name or email, from a ticket, or from a call.\n\n`ticket_id` searches by the requester's email or name; `call_id` hands the\ncaller's number to the phone seam, which answers `waiting` until Pigeonly's\nsearch covers phone numbers. A typed `q` or `phone` wins.","operationId":"search_customers_api_consumer_accounts_search_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"phone","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"}},{"name":"ticket_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},{"name":"call_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Call Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Customers Api Consumer Accounts Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}":{"get":{"tags":["consumer-accounts"],"summary":"Open Customer","description":"The customer's page: profile, the people they write to, billing, sends\nwith Roost's own print and tracking status, notes, and what customer service\nhas already done for them. A section Pigeonly would not answer (a role the\nstaff account lacks) carries its own error and never hides the rest.","operationId":"open_customer_api_consumer_accounts_customers__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Customer Api Consumer Accounts Customers  User Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/older":{"get":{"tags":["consumer-accounts"],"summary":"Open Older","description":"The customer's time before the move to the new site, read live from the old\nsystem through Pigeonly and never stored: their older orders (newest first,\neach with its lines and what was paid) and the notes customer service left\nthen. Each is its own section; the old system being unreachable is\n`legacy_unavailable` on that section, never an empty list.\n\nISS-1996: every Customer Dashboard open asks this, and it reads up to\n`OLDER_MAX_PAGES` pages at up to 20 seconds each, so it takes the brief\nadapter: on `_api` it held a pool slot for the whole read.","operationId":"open_older_api_consumer_accounts_customers__user_id__older_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Older Api Consumer Accounts Customers  User Id  Older Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/facilities/search":{"get":{"tags":["consumer-accounts"],"summary":"Search Facilities","description":"The facilities that match what the agent typed in Add a contact's \"Search\nfacilities\" box (`Customer Dashboard.dc.html`, dialog `addcontact`). Pigeonly's own\nsearch, read live and never stored: each hit's `id` is the `facility_address_id`\nthe new contact is made against. On the write grant, the one the act runs under: only a\nperson who may add a contact needs it. Under three characters it answers an empty list\n(`contact_add.FACILITY_QUERY_MIN`).","operationId":"search_facilities_api_consumer_accounts_facilities_search_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Facilities Api Consumer Accounts Facilities Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/legacy-call-reasons":{"get":{"tags":["consumer-accounts"],"summary":"Legacy Call Reasons","description":"The reasons the old system files a refund of an older order under, for the\nrefund dialog. On the money grant: only a person who may refund needs them.","operationId":"legacy_call_reasons_api_consumer_accounts_legacy_call_reasons_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Legacy Call Reasons Api Consumer Accounts Legacy Call Reasons Get"}}}}}}},"/api/consumer-accounts/customers/{user_id}/sends/{send_id}/content":{"get":{"tags":["consumer-accounts"],"summary":"Open Send Content","description":"What the customer sent in ONE of their sends (the letter text and the\nphoto links), read live for the order panel and never stored. 422 when the\nsend is not this customer's; 502 `permission_denied` when Pigeonly's account\nfor Roost lacks the role that reads content.\n\nThe letter's PDF link is NOT sent to the browser (issue #2004, 2026-10-08): the page is\ntold only `has_letter_pdf`, and asks for the file through `.../content/letter-pdf`, where\nthe server reads the link itself, checks it and fetches the object as Company OS.","operationId":"open_send_content_api_consumer_accounts_customers__user_id__sends__send_id__content_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"send_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Send Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Send Content Api Consumer Accounts Customers  User Id  Sends  Send Id  Content Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/sends/{send_id}/content/photos/{number}":{"get":{"tags":["consumer-accounts"],"summary":"Open Send Photo","description":"ONE picture the customer sent (photo `number`, counting from 1; a postcard's front and\nback follow the photos), as bytes with their picture type, for the order dialog.\n\nWHY. Pigeonly's photo links are bare links in a private bucket, so a browser gets 403\n(Frederick, 2026-10-07, issue #4223: \"these same images is what we're supposed to be able\nto see when customer service wants to look up people's orders ... i think that wasn't\nworking\"); Company OS's own bucket reader can read them. SECURITY (reasoning in\n`send_pictures`): the location comes from Pigeonly's answer for THIS customer's send,\nnever the request; it must be `https://storage.googleapis.com/<bucket>/<object>` on the\nallowed set (Pigeonly's own send buckets plus the Constitution's\n`gcs_fulfillment_bucket_allowlist`, the same set the letter PDF route uses); only picture\ntypes go back; the view is written to the outbox BEFORE a byte is returned. Answers: 422\nnot this customer's send or a link off the allow-list, 404 no such picture, 415 not a\npicture, 502 the bucket refused us, 503 no bucket credential, 403 the view could not be\nrecorded.","operationId":"open_send_photo_api_consumer_accounts_customers__user_id__sends__send_id__content_photos__number__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"send_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Send Id"}},{"name":"number","in":"path","required":true,"schema":{"type":"integer","title":"Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/sends/{send_id}/content/letter-pdf":{"get":{"tags":["consumer-accounts"],"summary":"Open Send Letter Pdf","description":"The letter of a migrated order as the PDF Pigeonly rendered, for the order dialog.\n\nWHY. The link on a migrated letter is a bare bucket link a browser cannot read (Frederick,\n2026-10-07, issue #4223: \"these same images is what we're supposed to be able to see when\ncustomer service wants to look up people's orders\"; issue #2004). SECURITY (reasoning in\n`send_letter_pdf`): the link comes from Pigeonly's answer for THIS customer's send, never the\nrequest; it must be on the allowed set (Pigeonly's own send buckets plus the Constitution's\nlist, the same set the picture route uses); only a PDF goes back; the view is written to the\noutbox BEFORE a byte is returned. Answers: 422 not this customer's send or a link off the\nlist, 404 no letter file, 415 not a PDF, 502 bucket refused us, 503 no credential, 403 the\nview could not be recorded.","operationId":"open_send_letter_pdf_api_consumer_accounts_customers__user_id__sends__send_id__content_letter_pdf_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"send_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Send Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/order-thumbnails":{"get":{"tags":["consumer-accounts"],"summary":"Order Thumbnails","description":"The thumbnails of the first `per_order` files of each order in `code` (repeat the\nparameter, at most 30), for the customer's page when it opens and for an order's dialog.\n\nWHY. Frederick, 2026-10-08, in session with the lead (not issue #4223): \"What do we need to\ndo to be able to quickly and easily see people's orders? Because that can't be a big loading\ntime delay either.\" One request for the\nwhole page, answered from this process's own short-lived copies (memory only, nothing stored);\na file with no copy yet is read from its bucket and drawn, all of them together, and waited\nfor `order_files.BUDGET_SECONDS`. What is not ready is `pending` (ask again in a second: it is\nkept being made), what Roost holds no file for is `none` (the older doors apply), and an\norder that is not this customer's is `refused` (the same word whether or not it exists).\n\nSECURITY (reasoning in `order_files`): the grant is `customer_accounts:read`; each code must\nbe among the print jobs Pigeonly lists for THIS customer; each file's bucket must be on the\nallowed set (the same set the picture and letter doors use); the audit row\n(`customer.order_thumbnails_viewed`) is written before any thumbnail goes back, and only when\none does. Answers: 422 more than 30 codes, a malformed code or a bad `per_order`; 403 the view\ncould not be recorded; 504 Pigeonly too slow to say whose order it is; and the consumer\nAPI's own refusals.","operationId":"order_thumbnails_api_consumer_accounts_customers__user_id__order_thumbnails_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"code","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"title":"Code"}},{"name":"per_order","in":"query","required":false,"schema":{"type":"integer","maximum":24,"minimum":1,"default":1,"title":"Per Order"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/orders/{code}/files/{n}/link":{"post":{"tags":["consumer-accounts"],"summary":"Order File Link","description":"A five-minute signed link to file `n` of the order `code`, so the browser opens the full\nfile straight from the bucket instead of through Company OS.\n\nWHY. A file sent through the API crosses the network twice and holds the API for the whole\ntransfer; a signed link is minted in about half a millisecond and the browser's first byte\ncomes in 55 to 100 ms (measured in production 2026-10-08). SECURITY (reasoning in\n`order_files` and `app.platform.gcs_signed_url`): same grant, ownership and allowed-bucket\nrules as the thumbnails; the link is GET-only for that one object, lives\n`gcs_signed_url.DEFAULT_TTL_SECONDS`, and is signed BEFORE the audit row\n(`customer.order_file_link_issued`) but returned only AFTER it is written, so a view that\ncannot be recorded gets no link. It is a POST because it mints a credential, and it WRITES (the\naudit row), so it takes the act's `_api` and `get_connection`, never the brief connection:\n`tests/platform/test_a_route_waiting_on_pigeonly_holds_no_pool_connection.py` (ISS-1685) holds\nthat no write depends on `get_brief_connection`, and a read-only key must reach its 403\nthrough the grant, not a 503 from the adapter builder (CI red on 644f85dc, fixed). Answers: 404 not\nthis customer's order or no such file, 422 a bucket off the allowed set or a malformed code,\n403 the view could not be recorded, 503 no bucket credential, 504 Pigeonly too slow.","operationId":"order_file_link_api_consumer_accounts_customers__user_id__orders__code__files__n__link_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"n","in":"path","required":true,"schema":{"type":"integer","title":"N"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/sends/{send_id}/resend":{"post":{"tags":["consumer-accounts"],"summary":"Resend Send","description":"Reprint a send through Roost. A reason is required and recorded with\nwho asked; 409 carries Pigeonly's own word (ALREADY_RESENT,\nNOTHING_TO_RESEND, NOT_A_MIGRATED_ORDER) in plain language.","operationId":"resend_send_api_consumer_accounts_customers__user_id__sends__send_id__resend_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"send_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Send Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendSendBody"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resend Send Api Consumer Accounts Customers  User Id  Sends  Send Id  Resend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/money-acts":{"post":{"tags":["consumer-accounts"],"summary":"Money Act","description":"A refund, an account credit (dollars, at most `CREDIT_LIMIT_CENTS`),\nfree months, or the refund of an order placed before the move to the new site\n(`legacy_refund`: the old order's number and a call reason; Pigeonly answers\n503 until it switches that on, which is `legacy_refunds_off` here, nothing\nrefunded). A refund names one of the customer's invoices (`invoices` on\nthe customer's page) and an amount no larger than its refundable part,\nfree months name one of their plans (`subscriptions`), else 422. Free\nmonths with no Recurly coupon named for that many answer 409\n`waiting_on_pigeonly` and are recorded as refused (who, for whom, why).\n\nA CREDIT THAT WOULD TAKE THE CUSTOMER PAST THE TOTAL (`credit_total`; Frederick,\n2026-10-07, issue #3757: \"an agent can give a customer up to $30 in credits in total,\never; any credit that would take them past $30 is held until Mark approves it\", confirmed\n\"Yes\") is neither sent nor refused. It answers 200 `{\"done\": false, \"held\": true,\n\"message\": ...}`: held for the customer service supervisor, who is asked on his Today\npage. `done` is true only when Pigeonly confirmed the act.\n\nA REFUND (of an invoice or of an older order) IS HELD THE SAME WAY past the customer's\nrefund total (Frederick, 2026-10-07, issue #3757: \"put same limit on refund no more then 3\nmonths before manager approval\"; the lead's reflection \"an agent can refund a customer up\nto $15 in total, ever, on their own; any refund that would take them past $15 is held until\nMark approves it\" he confirmed with \"Yes\"). Same answer, same card. Credits and refunds\nhave separate totals. No button is hidden.","operationId":"money_act_api_consumer_accounts_customers__user_id__money_acts_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoneyActBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Money Act Api Consumer Accounts Customers  User Id  Money Acts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/credit-holds/{act_id}/approve":{"post":{"tags":["consumer-accounts"],"summary":"Approve Credit Hold","description":"The customer service supervisor approves a credit OR A REFUND that was held for being\npast the customer's total, and it is sent (Frederick, 2026-10-07, issue #3757: \"No customer\nservice supervisor is mark\"). 403 `not_the_supervisor` for anyone else, 409 `not_waiting`\nwhen it was already decided (nothing is sent twice), and the same Pigeonly answers as\nany credit or refund when the send itself is refused. A refund whose invoice can no longer\nbe refunded (it was refunded meanwhile) fails plainly with 422 and nothing is sent. Called\nby the Today card. (The path keeps the name it had when only credits were held, so the\nreviewed API and its callers did not move; OPEN_ITEMS REF-8.)","operationId":"approve_credit_hold_api_consumer_accounts_credit_holds__act_id__approve_post","parameters":[{"name":"act_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Act Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Credit Hold Api Consumer Accounts Credit Holds  Act Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/credit-holds/{act_id}/decline":{"post":{"tags":["consumer-accounts"],"summary":"Decline Credit Hold","description":"The customer service supervisor declines a held credit or refund with his reason; nothing\nis sent (issue #3757). 422 without a reason, 403 for anyone else, 409 when it was already\ndecided. Called by the Today card.","operationId":"decline_credit_hold_api_consumer_accounts_credit_holds__act_id__decline_post","parameters":[{"name":"act_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Act Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeclineBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decline Credit Hold Api Consumer Accounts Credit Holds  Act Id  Decline Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/acts/write":{"post":{"tags":["consumer-accounts"],"summary":"Write Act","description":"An act that runs under `customer_accounts:write` and has no route of its own\n(the dashboard's Edit contact, Add a contact and the rest as each is registered).\n`acts.REGISTRY` decides which exist; today none does, so every kind is 422\n`invalid` and names what it waits on.","operationId":"write_act_api_consumer_accounts_customers__user_id__acts_write_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Write Act Api Consumer Accounts Customers  User Id  Acts Write Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/acts/money":{"post":{"tags":["consumer-accounts"],"summary":"Money Kind Act","description":"An act that runs under `customer_accounts:money` and has no route of its own\n(Take payment, Pause, Cancel, Upgrade and the rest as each is registered). As\n`write_act`: the registry decides; a kind that is not registered is 422.","operationId":"money_kind_act_api_consumer_accounts_customers__user_id__acts_money_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Money Kind Act Api Consumer Accounts Customers  User Id  Acts Money Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/consumer-accounts/customers/{user_id}/card":{"post":{"tags":["consumer-accounts"],"summary":"Update Card","description":"Replace the customer's card with the one a Recurly.js token stands for\n(Frederick, 2026-10-06, issue #4060). Card number, expiry and security code NEVER\narrive here: anything but the token, reason and ids, or any 13 to 19 digit run, is\n422 `card_data_refused` (fixed sentence, nothing repeated). The token is never\nlogged. 409 carries Pigeonly's word (a declined card); 502 `outcome_unknown` means\ndo not send it again. Body (read raw, so not in the schema): `token`, an optional\n`reason` (the design's Update card asks for none), and optional `request_id`,\n`ticket_id`, `call_id`, `suggestion_id` (the suggestion that was on screen, which\nrecords whether this act took it) and `contact_reason` (what the customer\ncontacted us about; both issue #4067).","operationId":"update_card_api_consumer_accounts_customers__user_id__card_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Card Api Consumer Accounts Customers  User Id  Card Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/customer-search":{"get":{"tags":["customer-search"],"summary":"Search","description":"One typed line, read every way it could be meant. Always 200 when the\nline is searchable: Pigeonly being down is `pigeonly.state`, never a 5xx, so\nthe mail senders and orders Roost holds still show.\n\n`pigeonly.state` is `ok`, `slow` (the account search was not answered in time) or\n`unavailable`; every OTHER part that did not come back is a note in `notes`\n(`part`, `state`, `count`), and the rows that did come back are all there.","operationId":"search_api_customer_search_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Api Customer Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/customer-search/senders/{key}":{"get":{"tags":["customer-search"],"summary":"Open Sender","description":"A mail sender: the return address as written, who they write to, their\nletters, and the app customer they are when the evidence says so.","operationId":"open_sender_api_customer_search_senders__key__get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Sender Api Customer Search Senders  Key  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/customer-search/customers/{user_id}/activity":{"get":{"tags":["customer-search"],"summary":"Customer Activity","description":"Every call, ticket, text, email and act Roost holds for this customer,\nfound by the phones and emails they are known by (read live from Pigeonly).","operationId":"customer_activity_api_customer_search_customers__user_id__activity_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Customer Activity Api Customer Search Customers  User Id  Activity Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/customer-search/customers/{user_id}/summary":{"get":{"tags":["customer-search"],"summary":"Customer Summary","description":"Rooster's few sentences on this customer (issue #4060; Frederick,\n2026-10-06: \"an AI summary ... Rooster can just take notes of what\nhappened\"). Read after the page, on its own request, because writing one\ntakes a model call the page must not wait for. Cached until the customer's\ntickets, calls or acts change (`summary.fingerprint`).\n\nALWAYS 200 for a customer Pigeonly knows: `{\"summary\": null}` when the model\ncannot answer or there is nothing to say yet, never an error, so the card\nsimply is not drawn.","operationId":"customer_summary_api_customer_search_customers__user_id__summary_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Customer Summary Api Customer Search Customers  User Id  Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/customer-search/customers/{user_id}/dashboard":{"get":{"tags":["customer-search"],"summary":"Customer Dashboard","description":"Everything the customer dashboard draws, in one read (issue #4067; design\n`docs/design/2026-10-07-customer-dashboard-handoff/`): the customer, their contacts\nwith each contact's own plan and billing, the orders, the payments, the tickets and\nthe History, Rooster's one suggestion, and which actions THIS person may draw.\n\nLive from Pigeonly and Recurly through the existing client; nothing is stored twice.\nEach section fails on its own (`{\"data\": null, \"error\": {...}}`) and the rest still\nanswers: \"Payments could not be reached\" is a state of the page, not a 5xx. 404 when\nPigeonly no longer has the customer. `actions` is the only thing the page uses to\ndecide what to draw: an action the service cannot do, or the person may not do, is\nfalse (`capabilities.py`).","operationId":"customer_dashboard_api_customer_search_customers__user_id__dashboard_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Customer Dashboard Api Customer Search Customers  User Id  Dashboard Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/customer-search/match":{"get":{"tags":["customer-search"],"summary":"Match Card","description":"Who a ticket or a call is about, so the card can appear without anyone\nsearching. `state`: one, several, none, waiting (Pigeonly cannot search a\nphone yet), unavailable.","operationId":"match_card_api_customer_search_match_get","parameters":[{"name":"ticket_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},{"name":"call_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Call Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Match Card Api Customer Search Match Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/book/auth":{"post":{"tags":["locke-booking"],"summary":"Authenticate","description":"Check the shared password and set the session cookie.","operationId":"authenticate_api_book_auth_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Authenticate Api Book Auth Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/book/signout":{"post":{"tags":["locke-booking"],"summary":"Signout","description":"Drop this session -- \"not you?\" on the form.\n\nDELETED SERVER-SIDE, not just unset in the browser. Clearing the cookie\nalone leaves a live row whose token is still valid, which matters because\nthe token is what names the person every booking is attributed to.\n\nDeliberately ungated: presenting no cookie, or a dead one, is not an error\nhere -- the caller is asking to have no session, and they end up with none\neither way.","operationId":"signout_api_book_signout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Signout Api Book Signout Post"}}}}}}},"/api/book/session":{"get":{"tags":["locke-booking"],"summary":"Session","description":"Who can be put in a meeting, once past the gate.\n\nReturns people with NO working hours too, marked unbookable. The design\ndims them with \"No working hours recorded yet, so they cannot be booked.\"\n-- hiding them would make a colleague look like they do not exist, and the\nfix would be invisible.","operationId":"session_api_book_session_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Session Api Book Session Get"}}}}}}},"/api/book/orgs":{"get":{"tags":["locke-booking"],"summary":"Orgs","description":"The full organisation list, with state and county on every row.","operationId":"orgs_api_book_orgs_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Orgs Api Book Orgs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/book/availability":{"get":{"tags":["locke-booking"],"summary":"Availability","description":"Half-hours when every selected host is free.","operationId":"availability_api_book_availability_get","parameters":[{"name":"host_ids","in":"query","required":false,"schema":{"type":"string","default":"","title":"Host Ids"}},{"name":"tz","in":"query","required":false,"schema":{"type":"string","default":"UTC","title":"Tz"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Availability Api Book Availability Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/book":{"post":{"tags":["locke-booking"],"summary":"Book","description":"Book it.\n\n409 `{\"error\": \"slot_taken\"}` is the one failure the page recovers from by\nreloading the grid rather than apologising, and neither it nor a 5xx may\ncost the booker their typing.","operationId":"book_api_book_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Book Api Book Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sms/incoming":{"post":{"tags":["sms"],"summary":"Incoming Sms","description":"Forward one text message to the mailbox that has always received them.\n\nALWAYS ANSWERS 200 ONCE THE REQUEST IS AUTHENTIC. Plivo retries a non-2xx,\nand a retry would resend the SAME message every time the mail path\nrecovered -- so a forwarding failure is logged loudly and acknowledged,\nbecause a mailbox holding six copies of one text is worse than a mailbox\nmissing it and an error in the log.\n\n⛔THAT PROMISE WAS FALSE FOR ONE PATH, until this build task (2026-09-22).\n`email_out.send_email` only logs when a send fails PERMANENTLY\n(`is_permanently_undeliverable` matched); an ordinary TRANSIENT failure\n(a SendGrid timeout, a 5xx) returns `SendOutcome(claimed=True, sent=\nFalse)` with nothing logged anywhere, and this route's own `except\nException` above never fires for it because `send_email` had already\ncaught the error itself. The only thing that happened, on the vendor's\nmachine and in this one's memory both, was `logger.info(\"...forwarded a\ntext...\")` -- a log line asserting success for a message that never\nleft. A text that failed to forward during any transient mail outage was\ntherefore indistinguishable from one that forwarded cleanly: 200 to\nPlivo (correct -- see above), an info log claiming delivery (wrong), and\nnothing durable a human could ever have found short of reading\nsend_receipts by hand. Fixed below by branching on `SendOutcome.sent`\nBEFORE assuming success, and by `SendOutcome.permanent` (not a string\nmatch at this call site) for whether the retry the next text's own\nattempt provides is worth waiting on.","operationId":"incoming_sms_api_sms_incoming_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Incoming Sms Api Sms Incoming Post"}}}}}}},"/api/sms/status":{"post":{"tags":["sms"],"summary":"Sms Status","description":"Plivo's delivery report for one text we sent.\n\n⛔NOTHING HAS EVER REPORTED ON A SENT TEXT. Measured 2026-09-21: this\nmodule served exactly one route (`/incoming`, texts arriving) and\n`app/platform/sms_out.py` asked Plivo for no callback at all, so there\nwas nowhere for a delivery report to land and no way to know whether a\ntext reached anybody. `sms_sends.status = 'sent'` has always meant \"Plivo\naccepted it from us\".\n\n⛔AND IT IS THE INSTRUMENT FOR THE COST QUESTION, WHICH IS WHY IT SHIPS\nBEFORE ANY COST COLUMN EXISTS. Worker 1, 2026-09-21: \"Learn it from\ntraffic, not from a purchase.\" We do not know, without buying a lookup,\nwhat Plivo names its per-message billing fields or how it reports\nsegments. So this logs the NAMES of every field it did not recognise, the\nmoney-shaped ones loudly, exactly as the hangup callback now does -- and\nthe first real multi-segment text tells us what to store. Storing it needs\ncolumns on `sms_sends`, which needs a migration number this builder does\nnot hold.\n\n⛔NAMES, NEVER VALUES, and the reason is sharper here than on a call: a\ndelivery report is about a text message, and a handler that logged\nunrecognised field values would eventually log somebody's message body.\n\nALWAYS 200 ONCE AUTHENTIC. Plivo retries a non-2xx, and a retried delivery\nreport is not a second delivery.","operationId":"sms_status_api_sms_status_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Sms Status Api Sms Status Post"}}}}}}},"/api/orgs/organizations":{"post":{"tags":["orgs"],"summary":"Create Organization","operationId":"create_organization_api_orgs_organizations_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrganizationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Organization Api Orgs Organizations Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["orgs"],"summary":"List Organizations","operationId":"list_organizations_api_orgs_organizations_get","parameters":[{"name":"lifecycle_stage","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Lifecycle Stage"}},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction"}},{"name":"org_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Org Type"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"account_manager_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Account Manager Principal Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"renewals_in_90_days","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Renewals In 90 Days"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Organizations Api Orgs Organizations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/names":{"get":{"tags":["orgs"],"summary":"List Organization Names","description":"Every organisation's id and name, unpaged. The one job is resolving\nan `org_id` a wire shape carries into something a person can read.\n\nNOT `GET /organizations?limit=200`, which is what the web app used and\nwhich cannot do this: `MAX_PAGE_LIMIT = 200` clamps that endpoint\nserver-side, there are 1,507 organisations, and `useOrgNameLookup` fell\nback to printing the raw UUID for every one past the first page. The\nLeads board showed `cb4932c7-874e-459b-89e5-0ddb1ca6e04b` where it meant\n\"California Department of Corrections and Rehabilitation\"; Adams County\nresolved only because it sorts early. Frederick, 2026-08-04: \"It looks\nlike you're not holding true names.\"\n\nPaging is the wrong shape for this question, so this endpoint has none.\nTwo columns over 1,507 rows is a small response, it is requested once\nper screen, and it can never silently truncate -- which is the entire\nfailure being fixed. Third occurrence of this defect class (PR #205 for\nfulfillment/SlaPage.tsx, then tickets' own org_name JOIN); doing it in\none place this time fixes all 32 call sites at once.\n\n⛔`state` TRAVELS WITH THE NAME, BECAUSE THE NAME IS NOT UNIQUE. 210 names\ncover 675 organisations: there are eight Adams Counties, thirteen Madison\nCounties and twelve Washington Counties, in different states, and they are\ndifferent agencies rather than duplicate rows. A picker showing bare names\noffers the same word eight times and no way to tell which one is meant.\nFrederick, 2026-08-31: \"adams has more than one on the list. The list\nshould match the organization list.\"\n\nDeclared BEFORE GET /organizations/{org_id} for the same routing reason\n/organizations/states is -- otherwise \"names\" is parsed as a UUID.","operationId":"list_organization_names_api_orgs_organizations_names_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Organization Names Api Orgs Organizations Names Get"}}}}}}},"/api/orgs/organizations/states":{"get":{"tags":["orgs"],"summary":"List Organization States","description":"Feeds the Organizations list's State filter (a searchable combobox,\nnot a hardcoded 50-state dropdown -- see service.list_distinct_states).\nDeclared BEFORE GET /organizations/{org_id} below: FastAPI/Starlette\nmatches routes in registration order, and {org_id} has no path-level\ntype constraint of its own (only the Python parameter is typed UUID),\nso a request for /organizations/states would otherwise be captured by\nthe dynamic route and 422 trying to parse \"states\" as a UUID -- exactly\nthe ordering PR #205's org-name JOIN fix had to get right.","operationId":"list_organization_states_api_orgs_organizations_states_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Organization States Api Orgs Organizations States Get"}}}}}}},"/api/orgs/organizations/deleted":{"get":{"tags":["orgs"],"summary":"List Deleted Organizations","description":"The trash. Registered ahead of GET /organizations/{org_id} (same\nreasoning as GET /api/cs/tickets/spam ahead of /tickets/{ticket_id}, and\nas /organizations/states just above): \"deleted\" must never parse as a\nUUID path param.","operationId":"list_deleted_organizations_api_orgs_organizations_deleted_get","parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Deleted Organizations Api Orgs Organizations Deleted Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}":{"get":{"tags":["orgs"],"summary":"Get Organization","operationId":"get_organization_api_orgs_organizations__org_id__get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Organization Api Orgs Organizations  Org Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["orgs"],"summary":"Update Organization","operationId":"update_organization_api_orgs_organizations__org_id__patch","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrganizationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Organization Api Orgs Organizations  Org Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["orgs"],"summary":"Delete Organization","description":"Soft delete, confirmed client-side before this ever fires (the\nfrontend names the org being deleted, plus what it hides, in a\nwindow.confirm — see OrganizationDetailPage.tsx). Blocked with 409 if\nthe org still has a live contract, an unpaid invoice, or an outstanding\ncommission obligation (service._organization_financial_holds) — the\ndetail message names exactly what.","operationId":"delete_organization_api_orgs_organizations__org_id__delete","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Organization Api Orgs Organizations  Org Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/account-managers":{"get":{"tags":["orgs"],"summary":"List Assignable Account Managers","description":"Who an organization's account manager can be set to.\n\nGated on `orgs:read`, deliberately NOT on `admin:people:read`. The only\nother principals roster is GET /api/platform/principals, which requires\nadmin:people:read -- a grant an ACCOUNT MANAGER does not hold. Feeding the\npicker from there would have broken it for precisely the people who work\non this page, the same way the AM column once showed them raw UUIDs.\n\nSCOPED TO THE Account Manager ROLE (build task \"flexible Actions menu +\nOrganizations list\", 2026-08-09) -- this used to return every active\nhuman (19), so both the Organizations list's account-manager FILTER and\nthe \"set account manager\" picker in OrganizationFormModal offered\npeople who do not do this job. Frederick's hand-created \"Account\nManager\" role (migrations/0046_ticket_access_by_role.sql's own comment)\nis exactly the 2 real ones -- `principal_roles`/`roles`, the same\nmembership `registry.py`'s own role machinery reads everywhere else,\nnot a second roster.\n\nReturns names and ids only: enough to choose someone, and nothing about\ntheir access, contact details or employment.","operationId":"list_assignable_account_managers_api_orgs_account_managers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Assignable Account Managers Api Orgs Account Managers Get"}}}}}}},"/api/orgs/organizations/{org_id}/restore":{"post":{"tags":["orgs"],"summary":"Restore Organization","description":"Reachable from GET /api/orgs/organizations/deleted (the trash) —\nsee DeletedOrganizationsPage.tsx. Also restores exactly the facilities\nand contacts this org's delete cascaded (service.restore_organization).","operationId":"restore_organization_api_orgs_organizations__org_id__restore_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restore Organization Api Orgs Organizations  Org Id  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}/communications":{"get":{"tags":["orgs"],"summary":"Read Org Communications","description":"What was actually said to this account, and who owes whom a reply.\n\n⛔THE BUSIEST ACCOUNT IN THE COMPANY SAID \"No contact logged yet\". The\nCommunications tab read `org_contact_events` -- rows a person types by\nhand -- and there are none, for any organisation. Iowa DOC has 710 ticket\nmessages and seven rows in `email_activity`, and the page showed a blank.\n\nFrederick, 2026-09-05: \"when I go to communications, I'm not seeing a\ncommunication history and summary on the organization ... I should see the\nmost recent communications to see the status of the account ... and I\nshould see the most recent temperature of that account.\"\n\nFour sources, because an account's history genuinely lives in four\nplaces: support threads, direct mail in a delegated mailbox, what somebody\nlogged by hand, and the invoices we sent. Every row carries the id of the\nrecord it came from, so anything citing this points at evidence rather\nthan at a rendered sentence.","operationId":"read_org_communications_api_orgs_organizations__org_id__communications_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":40,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Org Communications Api Orgs Organizations  Org Id  Communications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}/contact-events":{"get":{"tags":["orgs"],"summary":"List Org Contact Events","operationId":"list_org_contact_events_api_orgs_organizations__org_id__contact_events_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Org Contact Events Api Orgs Organizations  Org Id  Contact Events Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["orgs"],"summary":"Append Org Contact Event","description":"Always writes actor_kind='human' with the AUTHENTICATED principal's\nown id -- see AppendOrgContactEventRequest's docstring for why that is\nnot a request-body field. A Rooster-authored row can only ever be\nwritten by a caller inside this codebase invoking\nservice.append_org_contact_event directly with actor_kind='rooster'.","operationId":"append_org_contact_event_api_orgs_organizations__org_id__contact_events_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppendOrgContactEventRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Append Org Contact Event Api Orgs Organizations  Org Id  Contact Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/facilities":{"post":{"tags":["orgs"],"summary":"Create Facility","operationId":"create_facility_api_orgs_facilities_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFacilityRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Facility Api Orgs Facilities Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["orgs"],"summary":"List Facilities","operationId":"list_facilities_api_orgs_facilities_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"contracted_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Contracted Only"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Facilities Api Orgs Facilities Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/facilities/deleted":{"get":{"tags":["orgs"],"summary":"List Deleted Facilities","description":"The trash. Registered ahead of GET /facilities/{facility_id} — same\nreasoning as GET /organizations/deleted ahead of /organizations/{org_id}\nabove: \"deleted\" must never parse as a UUID path param. Reachable from\nDeletedRecordsPage.tsx's Facilities tab — see service.list_deleted_\nfacilities's own docstring for why this is scoped to a facility deleted\non its OWN, not one hidden by its whole org being trashed.","operationId":"list_deleted_facilities_api_orgs_facilities_deleted_get","parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Deleted Facilities Api Orgs Facilities Deleted Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/facilities/{facility_id}":{"get":{"tags":["orgs"],"summary":"Get Facility","operationId":"get_facility_api_orgs_facilities__facility_id__get","parameters":[{"name":"facility_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Facility Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Facility Api Orgs Facilities  Facility Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["orgs"],"summary":"Update Facility","operationId":"update_facility_api_orgs_facilities__facility_id__patch","parameters":[{"name":"facility_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Facility Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFacilityRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Facility Api Orgs Facilities  Facility Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["orgs"],"summary":"Delete Facility","operationId":"delete_facility_api_orgs_facilities__facility_id__delete","parameters":[{"name":"facility_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Facility Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Facility Api Orgs Facilities  Facility Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/facilities/{facility_id}/mailing-address":{"patch":{"tags":["orgs"],"summary":"Update Facility Mailing Address","description":"Org record right rail's edit-in-place mailing address (design review\nbuild direction, Cause 3 + worked example): dedicated from the generic\n`update_facility` PATCH above so every call stamps\n`mailing_address_verified_at` and emits the distinct\n`facility.mailing_address_updated` outbox event — see\napp.modules.orgs.service.update_facility_mailing_address.","operationId":"update_facility_mailing_address_api_orgs_facilities__facility_id__mailing_address_patch","parameters":[{"name":"facility_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Facility Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFacilityMailingAddressRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Facility Mailing Address Api Orgs Facilities  Facility Id  Mailing Address Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/facilities/{facility_id}/restore":{"post":{"tags":["orgs"],"summary":"Restore Facility","description":"Reachable from GET /api/orgs/facilities/deleted (the trash) — see\nDeletedRecordsPage.tsx's Facilities tab. Undoes a human's single-facility\ndelete OR app/modules/enrichment/service.py's roster-reconciliation\nsoft-delete (Frederick, 2026-08-10, on the latter: \"I can check the list\nand see if I wanna undelete anything\"). 404s if the parent organization\nis itself still deleted — restore that first.","operationId":"restore_facility_api_orgs_facilities__facility_id__restore_post","parameters":[{"name":"facility_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Facility Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restore Facility Api Orgs Facilities  Facility Id  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contacts":{"post":{"tags":["orgs"],"summary":"Create Contact","description":"The person, and what they are FOR, in one request (ISS-396).\n\n`roles` is optional and usually absent. When it is present the position\nis written by contact_roles_service.set_contact_roles inside the create's\nown transaction, so its 409s (PrimaryAlreadyTaken naming the colleague\nwho holds the slot, BillingRankRequired asking which of two is primary)\nreach the caller with NOTHING written -- the account manager answers the\nquestion and resubmits one form, rather than being left with a person\nadded and a position that never landed.","operationId":"create_contact_api_orgs_contacts_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContactRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Contact Api Orgs Contacts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["orgs"],"summary":"List Contacts","operationId":"list_contacts_api_orgs_contacts_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Contacts Api Orgs Contacts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contacts/deleted":{"get":{"tags":["orgs"],"summary":"List Deleted Contacts","description":"The trash. Registered ahead of GET /contacts/{contact_id} so \"deleted\"\nnever parses as a UUID path param (same reasoning as\nGET /organizations/deleted above).","operationId":"list_deleted_contacts_api_orgs_contacts_deleted_get","parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Deleted Contacts Api Orgs Contacts Deleted Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contacts/{contact_id}":{"get":{"tags":["orgs"],"summary":"Get Contact","operationId":"get_contact_api_orgs_contacts__contact_id__get","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Contact Api Orgs Contacts  Contact Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["orgs"],"summary":"Update Contact","description":"Every profile field a human can legitimately know about a person --\nname, email, phone, job title, state, mailing address, champion flag --\neditable from here regardless of the contact's organization's lifecycle\nstage (build task 2026-08-10: \"every person should have a profile...\nRight now when I go to a person, I can't edit anything\"). Deliberately\nNOT gated the way \"Position\" is (contact_roles.ACTIVE_CUSTOMER_STAGES,\nPR #357) -- name/phone/email/title are knowable for anyone, prospect or\nnot. Title (`role_title`, edited here) and Position (what they do for\nus, edited through PUT .../roles) are two different facts.\n\n`mailing_address` -- one call-out worth being explicit about: this is a\nSECOND writer of `contacts.mailing_address`. The only prior writer,\n`customer_service.agent_acts.address_update`, is ticket-scoped and\nappends to `cs_contact_address_audit` for that reason (a b2c consumer\ndisputing what their address was on a given date needs that trail). An\naccount manager correcting a person's address from their profile here\ndoes NOT append to that audit table -- it is the same governance every\nother profile field on this endpoint already gets (a plain `updated_at`,\nno per-field history), not a new gap this field alone has.","operationId":"update_contact_api_orgs_contacts__contact_id__patch","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContactRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Contact Api Orgs Contacts  Contact Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["orgs"],"summary":"Delete Contact","description":"Soft delete, confirmed client-side before this ever fires (the\nfrontend names the person being deleted — see PersonProfilePage.tsx).\nNo financial guard: a contact is never itself invoiced or contracted.","operationId":"delete_contact_api_orgs_contacts__contact_id__delete","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Contact Api Orgs Contacts  Contact Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contacts/{contact_id}/restore":{"post":{"tags":["orgs"],"summary":"Restore Contact","description":"Reachable from GET /api/orgs/contacts/deleted (the trash) — see\nDeletedPeoplePage.tsx. Refuses (404, ForeignOrgMismatch) if the parent\norganization is itself still deleted -- restore that first.","operationId":"restore_contact_api_orgs_contacts__contact_id__restore_post","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restore Contact Api Orgs Contacts  Contact Id  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contacts/{contact_id}/roles":{"put":{"tags":["orgs"],"summary":"Set Contact Roles","description":"The COMPLETE set of roles a human says this contact holds.\n\n409 (PrimaryAlreadyTaken) naming the colleague already in the To line\nfor that tag, rather than silently demoting them -- one primary per tag\nper organisation is a fact about the agency, and taking it off someone\nis a decision, not a side effect of saving a form.","operationId":"set_contact_roles_api_orgs_contacts__contact_id__roles_put","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetContactRolesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Contact Roles Api Orgs Contacts  Contact Id  Roles Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}/suggest-contact-roles":{"post":{"tags":["orgs"],"summary":"Suggest Contact Roles","description":"Propose a tag for every untagged contact at this organisation, read\noff their `role_title`, as `pending` rows awaiting agreement.\n\nA WRITE grant because it writes rows -- but nothing it writes routes any\nmail: a suggestion sits at review_state 'pending', which is deliberately\noutside ROUTABLE_REVIEW_STATES. Confirming is what makes it real, and\nthat is a separate, attributed act below.","operationId":"suggest_contact_roles_api_orgs_organizations__org_id__suggest_contact_roles_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Suggest Contact Roles Api Orgs Organizations  Org Id  Suggest Contact Roles Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contact-roles/{role_id}/review":{"post":{"tags":["orgs"],"summary":"Review Contact Role","description":"Agree with one of Rooster's proposals, or overturn it.\n\nSame two-verb shape as POST /api/cs/suppressions/{id}/review (migration\n0149): confirm moves the row into ROUTABLE_REVIEW_STATES so it starts\nrouting, overturn ends it in the same transaction. Both stamp the\nreviewing principal's id -- agreeing is a person taking responsibility\nfor who receives an agency's invoices, not a rubber stamp on something\nalready happening.","operationId":"review_contact_role_api_orgs_contact_roles__role_id__review_post","parameters":[{"name":"role_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Role Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewContactRoleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Review Contact Role Api Orgs Contact Roles  Role Id  Review Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/industry/dedup/proposals":{"get":{"tags":["industry"],"summary":"List Dedup Proposals","description":"The review queue (build task bullets 1/2/4). `min_score` defaults to\n`DEFAULT_REVIEW_MIN_SCORE` (0.80 -- the measured line between\ncertain/near-certain and \"needs a human's read\", domain.py's own\ndocstring) rather than 0, so a caller that never thinks about the\nparameter still gets the workable ~96-row queue instead of the\n2,101-row wall. Pass `min_score=0` to see everything -- nothing is ever\nhidden permanently, only by default.\n\n`tier_counts`/`total_open` are computed over EVERY open proposal\nregardless of `min_score`, so `hidden_count` is an honest count of what\nthe current threshold is hiding, not just \"items not on this page\".","operationId":"list_dedup_proposals_api_industry_dedup_proposals_get","parameters":[{"name":"min_score","in":"query","required":false,"schema":{"type":"number","default":0.8,"title":"Min Score"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Dedup Proposals Api Industry Dedup Proposals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/industry/dedup/proposals/{proposal_id}/accept":{"post":{"tags":["industry"],"summary":"Accept Dedup Proposal","operationId":"accept_dedup_proposal_api_industry_dedup_proposals__proposal_id__accept_post","parameters":[{"name":"proposal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Proposal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptProposalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Accept Dedup Proposal Api Industry Dedup Proposals  Proposal Id  Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/industry/dedup/proposals/{proposal_id}/reject":{"post":{"tags":["industry"],"summary":"Reject Dedup Proposal","operationId":"reject_dedup_proposal_api_industry_dedup_proposals__proposal_id__reject_post","parameters":[{"name":"proposal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Proposal Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RejectProposalRequest"},{"type":"null"}],"title":" Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reject Dedup Proposal Api Industry Dedup Proposals  Proposal Id  Reject Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/industry/dedup/proposals/bulk-accept":{"post":{"tags":["industry"],"summary":"Bulk Accept Dedup Proposals","operationId":"bulk_accept_dedup_proposals_api_industry_dedup_proposals_bulk_accept_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkAcceptRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response Bulk Accept Dedup Proposals Api Industry Dedup Proposals Bulk Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/industry/dedup/proposals/bulk-reject":{"post":{"tags":["industry"],"summary":"Bulk Reject Dedup Proposals","operationId":"bulk_reject_dedup_proposals_api_industry_dedup_proposals_bulk_reject_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRejectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response Bulk Reject Dedup Proposals Api Industry Dedup Proposals Bulk Reject Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/industry/dedup/generate":{"post":{"tags":["industry"],"summary":"Generate Dedup Proposals","description":"Re-runs the seed + generate pass (service.run_organization_dedup)\nfrom a controlled, authenticated context (staging/an ops screen) rather\nthan only ever from the CLI tool. Never called against production\nautomatically by anything in this codebase — Frederick runs the data\npass himself after reviewing (build task discipline).","operationId":"generate_dedup_proposals_api_industry_dedup_generate_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Generate Dedup Proposals Api Industry Dedup Generate Post"}}}}}}},"/api/finance/contracts":{"post":{"tags":["finance"],"summary":"Create Contract","operationId":"create_contract_api_finance_contracts_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContractRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Contract Api Finance Contracts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["finance"],"summary":"List Contracts","operationId":"list_contracts_api_finance_contracts_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"commission_bearing","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Commission Bearing"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Contracts Api Finance Contracts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/contracts/{contract_id}":{"get":{"tags":["finance"],"summary":"Get Contract","operationId":"get_contract_api_finance_contracts__contract_id__get","parameters":[{"name":"contract_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contract Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Contract Api Finance Contracts  Contract Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["finance"],"summary":"Update Contract","operationId":"update_contract_api_finance_contracts__contract_id__patch","parameters":[{"name":"contract_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contract Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContractRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Contract Api Finance Contracts  Contract Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/contracts/{contract_id}/amendments":{"get":{"tags":["finance"],"summary":"List Amendments","operationId":"list_amendments_api_finance_contracts__contract_id__amendments_get","parameters":[{"name":"contract_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contract Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Amendments Api Finance Contracts  Contract Id  Amendments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/contracts/{contract_id}/rate-schedules":{"get":{"tags":["finance"],"summary":"List Rate Schedules","operationId":"list_rate_schedules_api_finance_contracts__contract_id__rate_schedules_get","parameters":[{"name":"contract_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contract Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Rate Schedules Api Finance Contracts  Contract Id  Rate Schedules Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices":{"post":{"tags":["finance"],"summary":"Create Invoice","operationId":"create_invoice_api_finance_invoices_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvoiceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Invoice Api Finance Invoices Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["finance"],"summary":"List Invoices","operationId":"list_invoices_api_finance_invoices_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"term_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Term Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Invoices Api Finance Invoices Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/company-settings":{"get":{"tags":["finance"],"summary":"Get Company Settings Route","description":"Pigeonly's own billing identity (company_settings, single row) --\nthe SAME data app/modules/finance/invoice_pdf.py reads to render the\ngenerated PDF's header/footer. Read by the invoice detail page's\nrendered-invoice card (InvoiceDetailPage.tsx) and by Finance -> Company\nsettings (CompanySettingsPage.tsx).\n\n⛔BANK NUMBERS GO OUT AS THEIR LAST FOUR DIGITS ONLY (Frederick,\n2026-09-24: account and routing numbers are masked on screen and are\nwrite-only). Until that day this route returned both in full to anyone\nholding `finance:read`. The PDF is rendered server-side and still prints\nthem in full; that is the document a customer pays from.","operationId":"get_company_settings_route_api_finance_company_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Company Settings Route Api Finance Company Settings Get"}}}}}},"put":{"tags":["finance"],"summary":"Update Company Settings Route","description":"Finance -> Company settings: save edits to Pigeonly's billing identity\n-- company name, the invoice/remittance mailing address, remittance\npayee/address/instructions, bank name, ACH routing and account numbers,\nthe invoice contact email and mailing instructions.\n\nGATED ON `finance:write`, the Finance section's existing write key (held\nby the Finance role and by Frederick), for everything EXCEPT the bank.\n\n⛔THE BANK IS FREDERICK'S ALONE. Frederick, 2026-09-24: \"Nobody can\nchange bank details except me.\" A save that touches a bank field needs\n`finance:bank:write` as well -- checked in the service\n(company_settings.update_company_settings) against the REAL signed-in\nperson, so viewing as Frederick does not lend his key. Refused with 403\nand nothing written. Every bank change that does go through DMs\nFrederick (send_bank_change_alarm); the response's `bank_alarm` says\nwhether it reached him.\n\nEvery changed field is audited (company_settings_changes: who, when,\nwhich field, old/new -- bank numbers as last four). A bad routing or\naccount number refuses the whole save with 422 and writes nothing.","operationId":"update_company_settings_route_api_finance_company_settings_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanySettingsIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Update Company Settings Route Api Finance Company Settings Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/company-settings/changes":{"get":{"tags":["finance"],"summary":"List Company Settings Changes","description":"The audit of Finance -> Company settings, newest first: who changed\nwhich field, when, from what to what. Bank numbers appear as last four\ndigits only -- the table cannot hold more (migration 0597's CHECK).","operationId":"list_company_settings_changes_api_finance_company_settings_changes_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Company Settings Changes Api Finance Company Settings Changes Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/dashboard":{"get":{"tags":["finance"],"summary":"Get Invoices Dashboard","description":"design/Finance.dc.html Invoices landing dashboard (aging, top\ndebtors, pending-next-month, upcoming renewals, recent activity, KPIs).\nRegistered ahead of `/invoices/{invoice_id}` so the literal path always\nwins. `range_days` mirrors the design's 30d/60d/90d range tabs; default\nis 90 to match v1's own default window (packages/api/src/routes/\ninvoices.ts handleInvoiceDashboard: `parseInt(req.query.days, 10) || 90`)\n— Frederick, 2026-07-30, one-for-one parity with v1's Invoices page.","operationId":"get_invoices_dashboard_api_finance_invoices_dashboard_get","parameters":[{"name":"range_days","in":"query","required":false,"schema":{"type":"integer","default":90,"title":"Range Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Invoices Dashboard Api Finance Invoices Dashboard Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/manual-prefill":{"get":{"tags":["finance"],"summary":"Get Invoices Manual Prefill","description":"v1's GET /invoices/manual-prefill. Registered ahead of GET\n/invoices/{invoice_id} so the literal path is never eaten by the uuid\nroute — same reason v1's own comment gives for its route ordering.","operationId":"get_invoices_manual_prefill_api_finance_invoices_manual_prefill_get","parameters":[{"name":"term_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Term Id"}},{"name":"invoice_date","in":"query","required":true,"schema":{"type":"string","format":"date","title":"Invoice Date"}},{"name":"piece_count","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Piece Count"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Invoices Manual Prefill Api Finance Invoices Manual Prefill Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/generate-from-term":{"post":{"tags":["finance"],"summary":"Generate Invoice From Term Route","description":"/invoices/new. v1's POST /invoices/generate-from-contract, mapped\nonto a term line (routes.py module docstring).","operationId":"generate_invoice_from_term_route_api_finance_invoices_generate_from_term_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateFromTermRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Generate Invoice From Term Route Api Finance Invoices Generate From Term Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/issue-manual":{"post":{"tags":["finance"],"summary":"Issue Manual Invoice Route","description":"/invoices/issue-manual. v1's POST /invoices/issue-manual: validates\nthe typed dollar figures FIRST (validation.validate_manual_invoice,\nported verbatim from v1's manual-invoice.ts — the pre-existing bare\nCreateInvoiceModal / POST /invoices path has no equivalent check at\nall), then the org-lifecycle gate, then creates the invoice with\ndue_date passed through exactly as given.","operationId":"issue_manual_invoice_route_api_finance_invoices_issue_manual_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueManualInvoiceRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Issue Manual Invoice Route Api Finance Invoices Issue Manual Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/postage-waste":{"get":{"tags":["finance"],"summary":"Get Postage Waste Report","description":"The \"Postage waste\" report -- Frederick, 2026-09-30: \"that's\nsomething that I should be able to run a report on ... to make sure\nwe're not wasting postage as an organization.\" Same `finance:read`\npermission as every other finance report; see `postage_waste_report.py`\nfor exactly what each total counts and why.","operationId":"get_postage_waste_report_api_finance_postage_waste_get","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date","title":"Start"}},{"name":"end","in":"query","required":true,"schema":{"type":"string","format":"date","title":"End"}},{"name":"station","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Station"}},{"name":"buyer_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Buyer Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Postage Waste Report Api Finance Postage Waste Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/postage-waste/escalations":{"get":{"tags":["finance"],"summary":"Get Postage Waste Escalations","description":"Frederick, 2026-09-30: \"I should be able to see that to even know if\nthe fulfillment manager is not responding to it.\" Every label Rooster\nhas raised to the staff escalation chain\n(`app/modules/print/postage_waste.py`): when it was flagged, who it is\nwith now, how long it has been open, whether/who it escalated to, and\nwhat was done. Same `finance:read` gate as the totals above.","operationId":"get_postage_waste_escalations_api_finance_postage_waste_escalations_get","parameters":[{"name":"flagged_and_not_acted_on_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Flagged And Not Acted On Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Postage Waste Escalations Api Finance Postage Waste Escalations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/postage-waste/csv":{"get":{"tags":["finance"],"summary":"Get Postage Waste Report Csv","description":"The report's own \"Export CSV\" button -- same filters, same\npermission, as the JSON totals above; one row per purchased label.","operationId":"get_postage_waste_report_csv_api_finance_postage_waste_csv_get","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date","title":"Start"}},{"name":"end","in":"query","required":true,"schema":{"type":"string","format":"date","title":"End"}},{"name":"station","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Station"}},{"name":"buyer_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Buyer Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}":{"get":{"tags":["finance"],"summary":"Get Invoice","operationId":"get_invoice_api_finance_invoices__invoice_id__get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Invoice Api Finance Invoices  Invoice Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["finance"],"summary":"Update Invoice","operationId":"update_invoice_api_finance_invoices__invoice_id__patch","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInvoiceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Invoice Api Finance Invoices  Invoice Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/document":{"get":{"tags":["finance"],"summary":"Get Invoice Document","description":"Serves a PDF for one invoice -- the imported archive when one exists,\notherwise a fresh render matching v1's own invoice-pdf.ts byte-for-byte\n(Frederick, 2026-08-03: \"the PDFs need to match the PDFs of version\none... All the information that's on those PDFs needs to be there.\").\n\nFrederick, 2026-07-31: \"I don't have the ability to download the\ninvoice PDF that is in v1 and not in v2.\" Diagnosed as two invoices in\none gap: the 178 Invoiced.com-imported documents (migrations/\n0080_invoice_documents.sql) serve fine, but the other 37 -- including\nevery invoice the billing worker has generated since 2026-07-31 -- had\nno document anywhere, because v1 never stored one either: it rendered\nfresh via PDFKit (invoice-pdf.ts) on every request. This route ports\nthat behavior instead of only the archive.\n\nIMPORTED ORIGINALS ALWAYS WIN, checked first: those 178 are the actual\nbytes a customer received (an invoiced.com-branded document, not v1's\nown template -- confirmed by their own varying per-invoice remittance\ndetails, which a single generator reading one company_settings row\ncould never produce). Re-rendering over one would silently replace a\nreal historical document with a document nobody actually sent. Only an\ninvoice with NO archived row falls through to generation -- see\napp/modules/finance/invoice_pdf.py for the generator itself and its\nfidelity write-up.","operationId":"get_invoice_document_api_finance_invoices__invoice_id__document_get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}},{"name":"download","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Download"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/ar-history":{"get":{"tags":["finance"],"summary":"Get Invoice Ar History","description":"Read-only cadence-send / dunning history for one invoice (design\nreview build direction, worked example: the Org record Invoices tab's\nexpanded row needs \"latest-stage-wins dunning history\" in its trail).\nBacked by the real `ar_send_log` table (migrations/0005_finance.sql,\nwritten by service.queue_ar_send) — no new table, this is the read side\nof data that already exists but had no endpoint.","operationId":"get_invoice_ar_history_api_finance_invoices__invoice_id__ar_history_get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Invoice Ar History Api Finance Invoices  Invoice Id  Ar History Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/provenance":{"get":{"tags":["finance"],"summary":"Get Invoice Provenance","description":"master spec §3.9: 'production-to-billing parcel provenance in the\ninvoice evidence drawer' — 'per-piece invoices show which parcels/\nbatches rolled into the period.' Only a term line with pricing_model\n'per_parcel' (renamed from 'per_piece' by migrations/\n0077_contract_terms.sql) has a per-piece provenance story; a per_adp/\nflat (or term-less manual) invoice gets an honest `applicable: false`\ninstead of a fabricated parcel list. Calls\napp.modules.fulfillment.service directly (module-to-module composition,\nthe same cross-module pattern app/modules/print/service.py already uses\nagainst Fulfillment) rather than duplicating a parcels query here.","operationId":"get_invoice_provenance_api_finance_invoices__invoice_id__provenance_get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Invoice Provenance Api Finance Invoices  Invoice Id  Provenance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/parcel-count":{"post":{"tags":["finance"],"summary":"Set Invoice Parcel Count Route","description":"Put the month's parcel count into an existing per_parcel draft and\nreprice it from the contract.\n\nFrederick, 2026-08-04: \"Iowa is the only thing that remains in draft\nform... once I fill in the information, the amount of parcels, a send\nbutton on it.\" The billing tick raises that invoice on the 1st with\neveryone else's, before the count is knowable; this is how the real\nnumber gets into it afterwards. Repricing runs through the same\nderive_contract_billing the generator uses, so the rate and the line\nwording come from the contract, never from the caller.\n\nCHANGED 2026-10-04 (APPENDED; the quote above is the 2026-08-04 record):\nFrederick, 2026-10-04: \"I don't want to have it wait. Once I put the\nnumber, the number parcels in, it should just go out the next business day\nat 9 a.m. local time.\" Saving the count now also SCHEDULES the invoice's\nsend; the response carries `scheduled_send` (\"Sends Mon Oct 5, 9:00 AM\nCT\"). The Send button still works and cancels the schedule.","operationId":"set_invoice_parcel_count_route_api_finance_invoices__invoice_id__parcel_count_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetParcelCountRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Invoice Parcel Count Route Api Finance Invoices  Invoice Id  Parcel Count Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["finance"],"summary":"Clear Invoice Parcel Count Route","description":"Delete the month's parcel count from a per_parcel draft. The invoice goes\nback to $0 and its scheduled send is CANCELLED (Frederick, 2026-10-04: the\ncount is the send trigger, so removing it removes the send).","operationId":"clear_invoice_parcel_count_route_api_finance_invoices__invoice_id__parcel_count_delete","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Clear Invoice Parcel Count Route Api Finance Invoices  Invoice Id  Parcel Count Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/credits":{"post":{"tags":["finance"],"summary":"Credit Invoice Route","description":"Credit an invoice: it keeps its real amount and stops being owed.\n\nFrederick, 2026-09-08: \"not that we receive payment, that I can give\nPigeonly credits ... just so our accounting stays true.\" This never\ntouches invoice_payments, so a credit can never be mistaken for\ncollection in any figure that measures how well we get paid.\n\n⛔A PERSON ISSUES EVERY CREDIT. Frederick, 2026-09-28: \"No auto credit\non invoices ever. That must go through human.\" (ruling file\ndocs/01_decisions/rulings/2026-09-28-no-automatic-invoice-credits.md).\nAn agent principal is refused here whatever grants it holds -- a grant\nis not a person -- and this route is the ONLY caller of\n`service.credit_invoice` in app/ (pinned by\ntests/modules/finance/test_only_a_person_credits_an_invoice.py).","operationId":"credit_invoice_route_api_finance_invoices__invoice_id__credits_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditInvoiceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Credit Invoice Route Api Finance Invoices  Invoice Id  Credits Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["finance"],"summary":"List Invoice Credits Route","description":"The credit history for one invoice -- \"we credited this company this,\non this date, for this reason\".","operationId":"list_invoice_credits_route_api_finance_invoices__invoice_id__credits_get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Invoice Credits Route Api Finance Invoices  Invoice Id  Credits Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/void":{"post":{"tags":["finance"],"summary":"Void Invoice","operationId":"void_invoice_api_finance_invoices__invoice_id__void_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoidInvoiceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Void Invoice Api Finance Invoices  Invoice Id  Void Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/mark-sent":{"post":{"tags":["finance"],"summary":"Mark Invoice Sent","description":"v1 parity: POST /:id/mark-sent (packages/api/src/routes/invoices.ts:\n1713-1720). Gated by invoice_lifecycle.assert_sendable — v1's three\nsend-path gates (draft-only, org lifecycle, balance) all resolve to\nHTTP 422 there, not the generic 409 _handle_domain_errors gives\nArSendBlocked elsewhere (that mapping is for the AR pipeline's own\ninternal use, never exercised through a route today), so both\nInvoiceNotDraft and ArSendBlocked are mapped to 422 here directly.","operationId":"mark_invoice_sent_api_finance_invoices__invoice_id__mark_sent_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Invoice Sent Api Finance Invoices  Invoice Id  Mark Sent Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/copy":{"post":{"tags":["finance"],"summary":"Copy Invoice","operationId":"copy_invoice_api_finance_invoices__invoice_id__copy_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Copy Invoice Api Finance Invoices  Invoice Id  Copy Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/invoices/{invoice_id}/payments":{"post":{"tags":["finance"],"summary":"Record Payment","operationId":"record_payment_api_finance_invoices__invoice_id__payments_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordPaymentRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Payment Api Finance Invoices  Invoice Id  Payments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/queue":{"get":{"tags":["finance"],"summary":"Get Ar Scheduler Queue","operationId":"get_ar_scheduler_queue_api_finance_ar_scheduler_queue_get","parameters":[{"name":"filter","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Filter"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Ar Scheduler Queue Api Finance Ar Scheduler Queue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/templates":{"get":{"tags":["finance"],"summary":"Get Ar Scheduler Templates","operationId":"get_ar_scheduler_templates_api_finance_ar_scheduler_templates_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ar Scheduler Templates Api Finance Ar Scheduler Templates Get"}}}}}}},"/api/finance/ar-scheduler/templates/{step}":{"put":{"tags":["finance"],"summary":"Put Ar Scheduler Template","description":"v1 parity: PUT /templates/:step (ar-agent.ts:918-948).","operationId":"put_ar_scheduler_template_api_finance_ar_scheduler_templates__step__put","parameters":[{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveTemplateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Ar Scheduler Template Api Finance Ar Scheduler Templates  Step  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["finance"],"summary":"Delete Ar Scheduler Template","description":"Reset to Default -- v1 parity: DELETE /templates/:step\n(ar-agent.ts:952-962).","operationId":"delete_ar_scheduler_template_api_finance_ar_scheduler_templates__step__delete","parameters":[{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Ar Scheduler Template Api Finance Ar Scheduler Templates  Step  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/activity":{"get":{"tags":["finance"],"summary":"Get Ar Scheduler Activity","description":"The Email Audit Log tab -- v1 parity: GET /activity\n(ar-agent.ts:966-995), read from send_receipts (v2's own claim-once\nfloor, app/platform/email_out.py) instead of v1's activity_log table:\nevery real AR send already writes a 'sent' row there, keyed\n`ar:{invoice_id}:{cadence_step}` (ar_sender.ar_send_idempotency_key), so\nthis is the same durable record the send pipeline itself relies on,\nnot a second bookkeeping table that could drift out of sync with it.\nTest sends (POST .../test below) deliberately bypass send_receipts\nentirely (see that route's own docstring) so they never appear here.","operationId":"get_ar_scheduler_activity_api_finance_ar_scheduler_activity_get","parameters":[{"name":"filter","in":"query","required":false,"schema":{"type":"string","default":"week","title":"Filter"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"default":250,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Ar Scheduler Activity Api Finance Ar Scheduler Activity Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/drafts/{invoice_id}/{step}":{"put":{"tags":["finance"],"summary":"Put Ar Scheduler Draft","description":"Save Draft -- v1 parity: PUT /drafts/:invoiceId/:step\n(ar-agent.ts:1505-1524). Always un-cancels (see\nar_templates.save_draft_override's own docstring).","operationId":"put_ar_scheduler_draft_api_finance_ar_scheduler_drafts__invoice_id___step__put","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}},{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveDraftRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Ar Scheduler Draft Api Finance Ar Scheduler Drafts  Invoice Id   Step  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["finance"],"summary":"Delete Ar Scheduler Draft","description":"Cancel Send / \"Remove\" -- v1 parity: DELETE /drafts/:invoiceId/:step\n(ar-agent.ts:1528-1543). Soft: sets cancelled=true, never deletes any\ndrafted text.","operationId":"delete_ar_scheduler_draft_api_finance_ar_scheduler_drafts__invoice_id___step__delete","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}},{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Ar Scheduler Draft Api Finance Ar Scheduler Drafts  Invoice Id   Step  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/drafts/{invoice_id}/{step}/restore":{"post":{"tags":["finance"],"summary":"Restore Ar Scheduler Draft","description":"Restore a cancelled step -- v1 parity: POST\n/drafts/:invoiceId/:step/restore (ar-agent.ts:1547-1556).","operationId":"restore_ar_scheduler_draft_api_finance_ar_scheduler_drafts__invoice_id___step__restore_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}},{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restore Ar Scheduler Draft Api Finance Ar Scheduler Drafts  Invoice Id   Step  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/drafts/{invoice_id}/{step}/test":{"post":{"tags":["finance"],"summary":"Send Ar Scheduler Test","description":"Send Test -- v1 parity: POST /drafts/:invoiceId/:step/test\n(ar-agent.ts:1560-1606). Deliberately bypasses\napp/platform/email_out.send_email's claim-once floor: a test send must\nNEVER consume the real `ar:{invoice_id}:{cadence_step}` idempotency\nslot (that would permanently block the real send for this invoice/step\nthe moment someone clicked Test), matching v1's own test path (a bare\nsendGmailMessage call, no activity_log write at all) -- see\napp/modules/finance/ar_scheduler_send.py's send_test_email docstring.\n\nA STUBBED SEND MUST NEVER PRESENT AS A DELIVERED ONE. `build_ar_email_\nsender` returns `StubEmailSender` whenever any of its three gates is\noff -- `ar_customer_email_enabled` is false on production today, so\nthis route's sender IS the stub right now. Before this build task the\nresponse was an unconditional `{\"ok\": True}` regardless: Frederick\nclicked Send Test and got exactly that, with no mail and no error.","operationId":"send_ar_scheduler_test_api_finance_ar_scheduler_drafts__invoice_id___step__test_post","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}},{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Ar Scheduler Test Api Finance Ar Scheduler Drafts  Invoice Id   Step  Test Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/ar-scheduler/health":{"get":{"tags":["finance"],"summary":"Get Ar Scheduler Health","description":"v1 parity: GET /health (ar-agent.ts:1610-1659).","operationId":"get_ar_scheduler_health_api_finance_ar_scheduler_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ar Scheduler Health Api Finance Ar Scheduler Health Get"}}}}}}},"/api/finance/ar-scheduler/send-invoice":{"post":{"tags":["finance"],"summary":"Post Ar Scheduler Send Invoice","description":"Manual send -- v1 parity: POST /send-invoice (ar-agent.ts:999-1145).\nAlways sends the first `invoice` step; the automated cadence owns\nevery later step. Goes through the SAME claim-once floor, template\nresolution, CC/BCC and PDF-attachment path as the automated sender.\nDoes NOT send a still-`draft` invoice (a deliberate, pre-existing v2\ngate this build honors rather than loosens) -- see\napp/modules/finance/ar_scheduler_send.py send_manual_invoice's own\ndocstring for the exact v1 divergence.","operationId":"post_ar_scheduler_send_invoice_api_finance_ar_scheduler_send_invoice_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualSendInvoiceRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Ar Scheduler Send Invoice Api Finance Ar Scheduler Send Invoice Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/commissions/obligations":{"get":{"tags":["finance"],"summary":"List Commission Obligations","operationId":"list_commission_obligations_api_finance_commissions_obligations_get","parameters":[{"name":"recipient_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Recipient Principal Id"}},{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"term_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Term Id"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Commission Obligations Api Finance Commissions Obligations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/commissions/ledger":{"get":{"tags":["finance"],"summary":"Get Commission Ledger For Year","description":"The commissions page ledger, in v1's shape.\n\nRegistered ahead of `/commissions/ledger/{recipient_principal_id}` so the\nliteral path is not swallowed by the uuid route.\n\nSCOPED (Frederick, 2026-09-06): this used to sum EVERY recipient's\nobligations into one company-wide ledger regardless of who asked --\ncommission_ledger_for_year now pins it to the caller's own id unless\nthey hold `sees_all_commissions`, and returns `scope` on the wire so\nthe page can say whose figures these are.\n\n`recipient_principal_id` (2026-10-02, issue #2830): a sees_all_\ncommissions holder clicking one person on the \"People\" all-view asks\nfor THAT person's own page this way -- `commission_ledger_for_year`\nalready accepted this parameter internally (every call from\n`list_commission_people_for_year` uses it); this is just the wire\nfinally exposing it. A caller WITHOUT sees_all_commissions gets the\nidentical pinned-to-self result whether they pass this or not, exactly\nlike every other list-shaped commission read (commission_scope.\nresolve_scope's own rule) -- it can only ever narrow what a sees_all\nholder asked for onto one person, never widen what a scoped caller\nsees past their own rows.","operationId":"get_commission_ledger_for_year_api_finance_commissions_ledger_get","parameters":[{"name":"year","in":"query","required":true,"schema":{"type":"integer","title":"Year"}},{"name":"recipient_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Recipient Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Commission Ledger For Year Api Finance Commissions Ledger Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/commissions/people":{"get":{"tags":["finance"],"summary":"List Commission People","description":"The commissions page's \"People\" all-view (issue #2830, coordinator\nrelay 2026-10-02): one row per commission participant -- name, role,\nearned/paid/outstanding for `year` -- for a sees_all_commissions\nholder only. A scoped caller gets a 403, same shape as every other\ncommission-access refusal (commission_scope.CommissionAccessDenied):\n\"a scoped person never sees the People list\" (same relay), enforced\nserver-side rather than left to the frontend to decide not to ask.","operationId":"list_commission_people_api_finance_commissions_people_get","parameters":[{"name":"year","in":"query","required":true,"schema":{"type":"integer","title":"Year"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Commission People Api Finance Commissions People Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/commissions/ledger/{recipient_principal_id}":{"get":{"tags":["finance"],"summary":"Get Commission Ledger","operationId":"get_commission_ledger_api_finance_commissions_ledger__recipient_principal_id__get","parameters":[{"name":"recipient_principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Recipient Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Commission Ledger Api Finance Commissions Ledger  Recipient Principal Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/commissions/payouts":{"post":{"tags":["finance"],"summary":"Record Commission Payout","operationId":"record_commission_payout_api_finance_commissions_payouts_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordPayoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Commission Payout Api Finance Commissions Payouts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["finance"],"summary":"List Commission Payouts","description":"Read side of POST /commissions/payouts (design review build\ndirection: the Commissions ledger's \"Paid out\" figures and the \"All\nTime Paid\"/\"Paid Out <year>\" KPI cards need a real number, and there was\nno endpoint to read a recorded payout back at all before this).\n\nSCOPED (Frederick, 2026-09-06): same rule as the obligations list --\n`recipient_principal_id` is pinned to the caller's own id unless they\nhold `sees_all_commissions`.","operationId":"list_commission_payouts_api_finance_commissions_payouts_get","parameters":[{"name":"recipient_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Recipient Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Commission Payouts Api Finance Commissions Payouts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/organizations/{org_id}/offboard":{"post":{"tags":["finance"],"summary":"Offboard Customer Route","description":"Stop billing a customer who cancelled, and stop routing their mail.\n\nTHIS IS THE ONLY WAY TO SAY A CUSTOMER LEFT. `offboarding_records` is\ndescribed throughout the finance module as the sole authority that stops\nbilling, and until this route existed nothing in v2 could write one --\nevery row came from the v1 import. Three cancelled customers were found on\n2026-08-21 with no record at all, stopped only because their terms happened\nto be `expired`. Auto-renew had already minted one of them a fresh term for\nthe following year.\n\nONE WRITE, TWO EFFECTS, because the same record is read by both:\nfinance's invoice generator refuses to bill a period after the last\nbillable day, and transcription refuses to route newly scanned mail to\nthe facility (see transcription.service. _assert_facility_still_takes_\nscanned_mail). Baltimore County cost 1,373 parcels' worth of scanning,\nprinting and shipping for want of exactly this.\n\n`finance:write`: deciding a customer has left is the same authority as\ninvoicing them.","operationId":"offboard_customer_route_api_finance_organizations__org_id__offboard_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OffboardCustomerRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Offboard Customer Route Api Finance Organizations  Org Id  Offboard Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/company-numbers":{"get":{"tags":["finance"],"summary":"Get Company Numbers","description":"Both hand-entered cost tables, and what they total.\n\n⛔THE TWO TOTALS ARE RETURNED SEPARATELY AND NEVER SUMMED. The run rate is\na cash question; accrued money has not left the account. 05_DATA_SOURCES\n§1: folding accruals in \"would inflate the run rate and make the shortfall\nunreadable\", and the shortfall is the number the Company page exists to\nexplain.","operationId":"get_company_numbers_api_finance_company_numbers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Company Numbers Api Finance Company Numbers Get"}}}}}}},"/api/finance/company-numbers/variable-costs":{"get":{"tags":["finance"],"summary":"Get Company Variable Costs","description":"This month's variable cost, one line per vendor, for Company numbers.\n\nFrederick, 2026-09-24: \"We need the variable expenses ... all those\ndeserve their own line item for the company ... I want to bring the\ncompany page online, but I need the company numbers to be accurate.\" And:\n\"Just one line per vendor ... I just want to know what the total Plivo\nbill is.\"\n\nEach line is the vendor's own bill where the vendor reports one, Roost's\nown count where it does not, and \"not measured\" where neither exists --\nwith month to date, today, the month's run rate (a projection), the daily\nseries, and Roost's count beside the vendor's with drift over 5% flagged.\nSee `app/modules/finance/variable_cost_days.py`.\n\n⛔THE SAME GRANT AS THE FIXED LINES, for the reason `get_variable_cost`\ngives: this is the other half of one screen. It writes nothing; the days\nit reads are kept by the billing tick worker.","operationId":"get_company_variable_costs_api_finance_company_numbers_variable_costs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Company Variable Costs Api Finance Company Numbers Variable Costs Get"}}}}}}},"/api/finance/company-numbers/ubeo/open-invoices":{"get":{"tags":["finance"],"summary":"Get Ubeo Open Invoices","description":"UBEO's own open invoices -- past-due and current together, oldest due\ndate first. Frederick, 2026-09-24: \"Frederick wants to see UBEO's OPEN\ninvoices, both past-due and current: invoice number, date, due date,\namount, and whether a later email shows it paid.\"\n\nRead from email -- see `app/modules/finance/ubeo_invoices.py`.\n`paid_status_words` is the honest word for what the email trail can\nand cannot show: \"Open, unpaid\" when confirmed still owed, \"Paid\nstatus unknown from email\" when only ever named in a past-due\nreminder with no Payment Receipt since -- never a guessed \"paid\".\n\n⛔APPENDED 2026-09-24: Frederick sent UBEO's own PORTAL screenshots the\nsame day -- 11 open invoices, $14,610.89, all unpaid in full -- against\nthis reader's 4, $12,104.88. The gap: 7 invoices whose email Payment\nReceipt showed a $0.00 remaining balance for a check payment that (by\nevery sign available in the mailbox -- no reversal or bounce email\nexists anywhere) did not, in fact, clear -- something this reader has\nno way to see from email alone. Frederick's ruling: the PORTAL, read\nby Rooster's own browser (`app/modules/finance/ubeo_portal.py`), is\nthe PRIMARY source now; this email reader is the FALLBACK.\n\n⛔APPENDED AGAIN, SAME DAY: this route now reads `ubeo_invoices.\nubeo_summary`, not a hand-rolled total -- `ubeo_invoices` is the SAME\ntable both readers write into (by the same normalized invoice number),\nso the response is automatically portal-sourced once a portal read has\never landed a row, and email-sourced (as before) for anything the\nportal has not yet confirmed. `summary_words` is `UbeoSummary`'s own\n(\"UBEO: $X outstanding, $Y past due, next due <date>\", Frederick's own\nwording), never the older \"Email shows...\" line, which read as\nunderstating a figure the table may already have corrected.\n\n⛔THE SAME GRANT AS THE OTHER COMPANY-NUMBERS ROUTES. Writes nothing.","operationId":"get_ubeo_open_invoices_api_finance_company_numbers_ubeo_open_invoices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ubeo Open Invoices Api Finance Company Numbers Ubeo Open Invoices Get"}}}}}}},"/api/finance/company-numbers/ubeo/ricoh-costs":{"get":{"tags":["finance"],"summary":"Get Ubeo Ricoh Costs","description":"Cost per Ricoh printer, month to date and year to date -- Frederick,\n2026-09-24: \"Associate UBEO bills with the Ricoh printers ... show cost\nper Ricoh printer (month and year to date) ... in the Company numbers.\"\n\nEvery registered Ricoh press appears (even at $0.00, so a screen never\nreads \"no data\" as \"nothing was billed\" -- see `ubeo_equipment_match.\none_equipment_cost`'s own docstring), PLUS an `unassigned` bucket for\nevery UBEO invoice never matched to a machine (\"UBEO -- not tied to a\nprinter\", Frederick's own words, never guessed). If the floor's\nequipment register carries no Ricoh rows at all, `other_equipment`\nnames what DOES exist instead of silently returning nothing.","operationId":"get_ubeo_ricoh_costs_api_finance_company_numbers_ubeo_ricoh_costs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ubeo Ricoh Costs Api Finance Company Numbers Ubeo Ricoh Costs Get"}}}}}}},"/api/finance/company-numbers/stamps/import-balances-trend":{"post":{"tags":["finance"],"summary":"Import Stamps Balances Trend","description":"Upload Stamps.com's own \"BalancesTrend\" report -- a human export from\nits web dashboard (Reports tab), fetched by nobody automatically (see\n`stamps_balances_trend.py`'s own module docstring for what was checked).\n\nFrederick, 2026-09-24, having sent the report itself: \"This is the\nvendor's own daily ledger, and it's the authoritative Stamps.com\ncost.\" Parses the file, stores the vendor's own raw daily figures\n(idempotent -- a second upload of the SAME file changes nothing), and\nOVERRIDES `vendor_reported_days` for every day it covers -- a\ndeliberate correction from a more authoritative source, the one\nexception to the day-keeper's own \"kept once, never revisited\" rule.\nFlags any day whose report Ending Balance disagrees with Roost's own\nlast balance reading that day.","operationId":"import_stamps_balances_trend_api_finance_company_numbers_stamps_import_balances_trend_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_import_stamps_balances_trend_api_finance_company_numbers_stamps_import_balances_trend_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Import Stamps Balances Trend Api Finance Company Numbers Stamps Import Balances Trend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/my-pay":{"get":{"tags":["finance"],"summary":"My Pay","description":"What THIS person is paid. Their own row, and only ever their own.\n\n⛔NO GRANT, AND NO PARAMETER. Frederick, 2026-09-02: \"we can display that\nrate to them.\" Somebody seeing their own pay needs no permission -- they\nalready know it -- but the absence of a principal_id here is what makes\nthat true. A parameter would turn \"see your own pay\" into \"see anyone's\"\nthe first time somebody changed a number in a URL.\n\n⛔READ ONLY. There is no PUT beside this. Pay is written on Company\nnumbers by whoever holds that grant; a person editing their own would\nmake the figure that drives the company's cost estimate self-reported.","operationId":"my_pay_api_finance_my_pay_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Pay Api Finance My Pay Get"}}}}}}},"/api/finance/company-numbers/team":{"get":{"tags":["finance"],"summary":"List Team Pay","description":"Everyone active and what they are paid, for the Team salaries figure.\n\n⛔GATED LIKE THE REST OF THE COST SIDE. Pay is the most sensitive thing\nthis system holds about a person, and this endpoint returns all of it for\neverybody -- so it sits behind the same grant as the rest of Company\nnumbers and nothing weaker. A person's own rate reaches them through\ntheir profile, not through here.","operationId":"list_team_pay_api_finance_company_numbers_team_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Team Pay Api Finance Company Numbers Team Get"}}}}}}},"/api/finance/company-numbers/team/{principal_id}":{"put":{"tags":["finance"],"summary":"Set Team Pay","description":"Set what one person is paid.\n\n⛔NOBODY SETS THEIR OWN PAY, AND THAT IS NOT ABOUT TRUST. This figure\ndrives the company's cost estimate, and a number a person can change\nabout themselves stops being evidence of anything. It is written by\nwhoever holds Company numbers write, the same grant that maintains every\nother cost line.","operationId":"set_team_pay_api_finance_company_numbers_team__principal_id__put","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamPayIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Team Pay Api Finance Company Numbers Team  Principal Id  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming":{"get":{"tags":["finance"],"summary":"List Incoming","description":"The queue. Most severe dunning first. The summary is what filing\neverything WOULD add, split by Rooster's confidence; none of it is in any\nledger total until filed.","operationId":"list_incoming_api_finance_bills_incoming_get","parameters":[{"name":"state","in":"query","required":false,"schema":{"enum":["unconfirmed","filed","disputed","void","all"],"type":"string","default":"unconfirmed","title":"State"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Incoming Api Finance Bills Incoming Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming/{incoming_id}/file":{"post":{"tags":["finance"],"summary":"File Incoming","description":"Owe it: the bill (or its invoice) is written to the ledger and the row\nreads filed. A bill in dispute needs `agreed_amount`. Already filed or void\nanswers 409.","operationId":"file_incoming_api_finance_bills_incoming__incoming_id__file_post","parameters":[{"name":"incoming_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Incoming Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/FileIn"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response File Incoming Api Finance Bills Incoming  Incoming Id  File Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming/{incoming_id}/answer":{"post":{"tags":["finance"],"summary":"Answer Card","description":"Answer the Today card for a bill found in email (design drift audit, T6,\n2026-10-08): the same file and void the Payables queue calls, so a tap and a\nclick are one decision. 409 when the bill was already decided.","operationId":"answer_card_api_finance_bills_incoming__incoming_id__answer_post","parameters":[{"name":"incoming_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Incoming Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncomingAnswerIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Answer Card Api Finance Bills Incoming  Incoming Id  Answer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming/{incoming_id}/void":{"post":{"tags":["finance"],"summary":"Void Incoming","description":"Don't owe: void, with a reason. Never paid, never paydown, hidden from\nthe queue, and it frees the invoice number.","operationId":"void_incoming_api_finance_bills_incoming__incoming_id__void_post","parameters":[{"name":"incoming_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Incoming Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__finance__obligations__incoming_routes__VoidIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Void Incoming Api Finance Bills Incoming  Incoming Id  Void Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming/{incoming_id}/dispute":{"post":{"tags":["finance"],"summary":"Dispute Incoming","description":"In dispute: kept with its claimed amount, in no total, with a note.","operationId":"dispute_incoming_api_finance_bills_incoming__incoming_id__dispute_post","parameters":[{"name":"incoming_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Incoming Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Dispute Incoming Api Finance Bills Incoming  Incoming Id  Dispute Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming/{incoming_id}/review":{"post":{"tags":["finance"],"summary":"Reopen Incoming","description":"Move a decision back to \"to review\". An owed bill's real bill is taken\nout of every total (superseded, never deleted).","operationId":"reopen_incoming_api_finance_bills_incoming__incoming_id__review_post","parameters":[{"name":"incoming_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Incoming Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Reopen Incoming Api Finance Bills Incoming  Incoming Id  Review Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/incoming/{incoming_id}":{"patch":{"tags":["finance"],"summary":"Fix Incoming","description":"Fix Rooster's read (vendor, invoice number, amount, due date, kind,\ncategory, which bill). A corrected bill reads high confidence.","operationId":"fix_incoming_api_finance_bills_incoming__incoming_id__patch","parameters":[{"name":"incoming_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Incoming Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FixIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Fix Incoming Api Finance Bills Incoming  Incoming Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills":{"get":{"tags":["finance"],"summary":"List Bills","description":"The overview: stat cards, one row per bill (past due first, largest\noutstanding first), category subtotals, totals by kind, and the 12-week\ntrend. Every bill says which kind it is.","operationId":"list_bills_api_finance_bills_get","parameters":[{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"enum":["current","past_due"],"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Q"}},{"name":"lifecycle","in":"query","required":false,"schema":{"anyOf":[{"enum":["active","paused","ended"],"type":"string"},{"type":"null"}],"title":"Lifecycle"}},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"enum":["fixed_recurring","invoiced_variable","usage_reload"],"type":"string"},{"type":"null"}],"title":"Kind"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Bills Api Finance Bills Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["finance"],"summary":"Add Bill","description":"The accountant adds a bill to the budget. A bill for the same payee\nalready on the budget answers 409 with `existing_id`; send\n`confirm_duplicate: true` to add it anyway.","operationId":"add_bill_api_finance_bills_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Add Bill Api Finance Bills Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/payroll":{"get":{"tags":["finance"],"summary":"Get Payroll","description":"Salary owed per person and in total, most behind first: salary per\nperiod, periods behind, amount owed, oldest unpaid period, and the weekly\ntrend of total salary owed. Finance grant only.","operationId":"get_payroll_api_finance_bills_payroll_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Payroll Api Finance Bills Payroll Get"}}}}}}},"/api/finance/bills/payroll/runs":{"get":{"tags":["finance"],"summary":"Get Pay Runs","description":"Pay-run history: each run with who it paid, for which periods, and\nwhether the bank has been matched to it.","operationId":"get_pay_runs_api_finance_bills_payroll_runs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Pay Runs Api Finance Bills Payroll Runs Get"}}}}}},"post":{"tags":["finance"],"summary":"Add Pay Run","description":"Record a pay run: a date, a total, and which people and periods it paid.\nRefused (422) unless they add up to the total, every period is real and has\nfallen due, and nothing is paid beyond what a period still owes.","operationId":"add_pay_run_api_finance_bills_payroll_runs_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRunIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Add Pay Run Api Finance Bills Payroll Runs Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/payroll/runs/{run_id}":{"delete":{"tags":["finance"],"summary":"Remove Pay Run","description":"Take a pay run back (recorded in error). Its payments are voided with\nit and the arrears return.","operationId":"remove_pay_run_api_finance_bills_payroll_runs__run_id__delete","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Remove Pay Run Api Finance Bills Payroll Runs  Run Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/payroll/runs/{run_id}/bank-match":{"post":{"tags":["finance"],"summary":"Match Pay Run To Bank","description":"Say a bank debit IS this pay run (the accountant's confirmation).","operationId":"match_pay_run_to_bank_api_finance_bills_payroll_runs__run_id__bank_match_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankMatchIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Match Pay Run To Bank Api Finance Bills Payroll Runs  Run Id  Bank Match Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/payroll/debits":{"get":{"tags":["finance"],"summary":"Get Salary Debits","description":"Salary-type debits in the bank and where each stands: matched to a pay\nrun, asked about, or not looked at yet.","operationId":"get_salary_debits_api_finance_bills_payroll_debits_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Since"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Salary Debits Api Finance Bills Payroll Debits Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/payroll/debits/{question_id}/answer":{"post":{"tags":["finance"],"summary":"Answer Card","description":"Answer a salary-debit question from its Today card (design drift audit,\nT6, 2026-10-08). The Slack buttons and the thread reply end in the same\n`payroll.confirm_proposal`, so a tap here and an answer there settle ONE\nrecord. 409 when the question was already answered, the chosen pay runs no\nlonger fit the debit, or the choice is not one this question offers; the\nquestion is then still open.","operationId":"answer_card_api_finance_bills_payroll_debits__question_id__answer_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Question Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DebitAnswerIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Answer Card Api Finance Bills Payroll Debits  Question Id  Answer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/trend":{"get":{"tags":["finance"],"summary":"Get Trend","description":"Total outstanding at the end of each week: the climb out of the hole.","operationId":"get_trend_api_finance_bills_trend_get","parameters":[{"name":"weeks","in":"query","required":false,"schema":{"type":"integer","maximum":52,"minimum":2,"default":12,"title":"Weeks"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Trend Api Finance Bills Trend Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/transactions":{"get":{"tags":["finance"],"summary":"Get Transactions","description":"Outgoing payments the bank feed shows, each with the bill it is counted\nagainst. Empty, with `bank_connected: false`, until the feed is connected.","operationId":"get_transactions_api_finance_bills_transactions_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Since"}},{"name":"only_unmatched","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Only Unmatched"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Transactions Api Finance Bills Transactions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/suggestions":{"get":{"tags":["finance"],"summary":"Get Suggestions","description":"Recurring payees Rooster sees in the bank that are not yet bills. These\nare suggestions; none is a bill until the accountant accepts it.","operationId":"get_suggestions_api_finance_bills_suggestions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Suggestions Api Finance Bills Suggestions Get"}}}}}}},"/api/finance/bills/suggestions/{suggestion_id}/accept":{"post":{"tags":["finance"],"summary":"Accept Suggestion","operationId":"accept_suggestion_api_finance_bills_suggestions__suggestion_id__accept_post","parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Suggestion Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Accept Suggestion Api Finance Bills Suggestions  Suggestion Id  Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/suggestions/{suggestion_id}/dismiss":{"post":{"tags":["finance"],"summary":"Dismiss Suggestion","operationId":"dismiss_suggestion_api_finance_bills_suggestions__suggestion_id__dismiss_post","parameters":[{"name":"suggestion_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Suggestion Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Dismiss Suggestion Api Finance Bills Suggestions  Suggestion Id  Dismiss Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/invoices":{"get":{"tags":["finance"],"summary":"List Invoices","description":"One row per invoice across every invoiced bill: vendor, invoice #,\ndate, amount, due date, paid, outstanding, days past due.","operationId":"list_invoices_api_finance_bills_invoices_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"enum":["past_due","unpaid","paid"],"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Q"}},{"name":"bill_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Bill Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Invoices Api Finance Bills Invoices Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/invoices/{invoice_id}":{"patch":{"tags":["finance"],"summary":"Edit Invoice","operationId":"edit_invoice_api_finance_bills_invoices__invoice_id__patch","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Edit Invoice Api Finance Bills Invoices  Invoice Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["finance"],"summary":"Void Invoice","description":"Take an invoice off its bill (filed in error). Voided, not deleted.","operationId":"void_invoice_api_finance_bills_invoices__invoice_id__delete","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Void Invoice Api Finance Bills Invoices  Invoice Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/invoices/{invoice_id}/pdf":{"put":{"tags":["finance"],"summary":"Put Invoice Pdf","description":"Attach the invoice's PDF: the raw PDF bytes as the request body.","operationId":"put_invoice_pdf_api_finance_bills_invoices__invoice_id__pdf_put","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}},{"name":"file_name","in":"query","required":false,"schema":{"type":"string","maxLength":200,"default":"invoice.pdf","title":"File Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Put Invoice Pdf Api Finance Bills Invoices  Invoice Id  Pdf Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["finance"],"summary":"Get Invoice Pdf","operationId":"get_invoice_pdf_api_finance_bills_invoices__invoice_id__pdf_get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invoice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/{line_id}/invoices":{"post":{"tags":["finance"],"summary":"Add Invoice","description":"The accountant files an invoice under an invoiced bill. The same\nvendor and invoice number twice answers 409 with `existing_id`.","operationId":"add_invoice_api_finance_bills__line_id__invoices_post","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Add Invoice Api Finance Bills  Line Id  Invoices Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/details":{"get":{"tags":["finance"],"summary":"Bill Details","description":"Several bills with their math in one request (`?ids=a&ids=b`), in the\norder asked: what `GET /{line_id}` returns for each. The Payroll and Usage\ntabs read their whole grid with this instead of one request per row (PR\n#3736 review, 2026-10-04). Declared before `/{line_id}` so `details` is\nnot read as a bill id.","operationId":"bill_details_api_finance_bills_details_get","parameters":[{"name":"ids","in":"query","required":true,"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":200,"title":"Ids"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Bill Details Api Finance Bills Details Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/{line_id}":{"get":{"tags":["finance"],"summary":"Get Bill","description":"One bill with its math: periods and payments. An invoiced bill adds one\nrow per invoice; a usage bill adds spend per month, reload history,\nbalance where known and burn rate.","operationId":"get_bill_api_finance_bills__line_id__get","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Bill Api Finance Bills  Line Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["finance"],"summary":"Edit Bill","operationId":"edit_bill_api_finance_bills__line_id__patch","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Edit Bill Api Finance Bills  Line Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/{line_id}/opening-balance":{"put":{"tags":["finance"],"summary":"Set Opening Balance","description":"Record or adjust the debt that already existed when the bill was\ncreated, as of a date. Salary debt that predates Roost counts.","operationId":"set_opening_balance_api_finance_bills__line_id__opening_balance_put","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpeningBalanceIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Set Opening Balance Api Finance Bills  Line Id  Opening Balance Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/{line_id}/lifecycle":{"post":{"tags":["finance"],"summary":"Set Lifecycle","description":"Pause, resume or end a bill. What is already owed stays owed.","operationId":"set_lifecycle_api_finance_bills__line_id__lifecycle_post","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Set Lifecycle Api Finance Bills  Line Id  Lifecycle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/{line_id}/payments":{"post":{"tags":["finance"],"summary":"Add Payment","description":"Count a payment against this bill: a bank transaction Rooster could not\nmatch, or a payment made another way (a check, a cash payment).","operationId":"add_payment_api_finance_bills__line_id__payments_post","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Add Payment Api Finance Bills  Line Id  Payments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bills/{line_id}/payments/{payment_id}":{"delete":{"tags":["finance"],"summary":"Remove Payment","description":"Take a payment off this bill (a wrong match). It is voided, not deleted.","operationId":"remove_payment_api_finance_bills__line_id__payments__payment_id__delete","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}},{"name":"payment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Remove Payment Api Finance Bills  Line Id  Payments  Payment Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/vendors":{"get":{"tags":["finance"],"summary":"List Vendors","description":"Each vendor: terms, how it is paid, paid through, next due, payment\nhistory. Never the bank details or payout instructions.","operationId":"list_vendors_api_finance_vendors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Vendors Api Finance Vendors Get"}}}}}},"post":{"tags":["finance"],"summary":"Add Vendor","description":"Add a vendor and its bills line, from a display name alone (Frederick,\n2026-10-05: \"a place I can copy the link and text or email them myself\"). The\nvendor fills in everything else through its link; what he pays is set next on\nthe vendor's page. Returns the vendor's id and its bill's id (the page to go to).","operationId":"add_vendor_api_finance_vendors_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewVendorIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Add Vendor Api Finance Vendors Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/vendors/{vendor_id}/payment-settings":{"put":{"tags":["finance"],"summary":"Set Payment Settings","description":"Set how much a vendor is paid and how often (Frederick, 2026-10-05: \"I put\nin how much I'm paying and what intervals I'm gonna pay it\"). One amount, one\ninterval, a start date. He can change either whenever he likes; a change\napplies to the next unpaid period and is kept as history. Only the finance\ngrant reaches this; the vendor's own routes cannot.","operationId":"set_payment_settings_api_finance_vendors__vendor_id__payment_settings_put","parameters":[{"name":"vendor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Vendor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VendorPaymentSettingsIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Set Payment Settings Api Finance Vendors  Vendor Id  Payment Settings Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/vendors/{vendor_id}/agreement/document":{"put":{"tags":["finance"],"summary":"Attach Document","description":"Attach the agreement itself (a PDF, image or Word file, the request body)\nto the agreement in force.","operationId":"attach_document_api_finance_vendors__vendor_id__agreement_document_put","parameters":[{"name":"vendor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Vendor Id"}},{"name":"name","in":"query","required":false,"schema":{"type":"string","default":"agreement","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Attach Document Api Finance Vendors  Vendor Id  Agreement Document Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["finance"],"summary":"Get Document","operationId":"get_document_api_finance_vendors__vendor_id__agreement_document_get","parameters":[{"name":"vendor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Vendor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/vendors/{vendor_id}/link":{"post":{"tags":["finance"],"summary":"Mint Link","description":"A fresh registration link to hand the vendor, with the day it expires (the\ncontractor link's three-day setting). Roost does not send it: Frederick copies\nit and texts or emails it himself (\"a place I can copy the link and text or\nemail them myself\"). Also how a registered vendor's details are changed: the\nold link is spent. The URL is returned to the caller; it is not listed\nanywhere else.","operationId":"mint_link_api_finance_vendors__vendor_id__link_post","parameters":[{"name":"vendor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Vendor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Mint Link Api Finance Vendors  Vendor Id  Link Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt":{"get":{"tags":["finance"],"summary":"Overview","description":"The Debt tab: back debt today, the paydown line from plan start, arrears,\nthe plan lines in waterfall order, the SAFE rounds and the equity group, and\nthe waterfall with its gate.","operationId":"overview_api_finance_debt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Overview Api Finance Debt Get"}}}}}}},"/api/finance/debt/lines/{line_id}":{"get":{"tags":["finance"],"summary":"Line Detail","description":"One plan line: its five numbers, every instalment and how it is covered,\nand the payments entered against it.","operationId":"line_detail_api_finance_debt_lines__line_id__get","parameters":[{"name":"line_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Line Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Line Detail Api Finance Debt Lines  Line Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt/notes":{"get":{"tags":["finance"],"summary":"List Notes","description":"Every SAFE and equity note with its standing and evidence. A holder who\nmissed the contact deadline is listed, owes 0 and says why.","operationId":"list_notes_api_finance_debt_notes_get","parameters":[{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"enum":["safe","equity"],"type":"string"},{"type":"null"}],"title":"Kind"}},{"name":"standing","in":"query","required":false,"schema":{"anyOf":[{"enum":["owed","missed_contact_deadline"],"type":"string"},{"type":"null"}],"title":"Standing"}},{"name":"round_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Round Id"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Notes Api Finance Debt Notes Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt/payments":{"get":{"tags":["finance"],"summary":"List Payments","description":"Payments entered, newest first, with who entered them and when.","operationId":"list_payments_api_finance_debt_payments_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"line_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Line Id"}},{"name":"include_voided","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Voided"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Payments Api Finance Debt Payments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["finance"],"summary":"Post Payments","description":"Enter a batch of payments (Post all). All or nothing; backdating is just\nan earlier date; a split is rows sharing a `split_group`; an identical\npayment already entered is a 409 unless `confirm_duplicate`.","operationId":"post_payments_api_finance_debt_payments_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentsIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Payments Api Finance Debt Payments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt/payments/{payment_id}/void":{"post":{"tags":["finance"],"summary":"Void Payment","description":"Void one entered payment (who, when and why are kept). A correction is a\nvoid and a new entry; nothing is edited or deleted.","operationId":"void_payment_api_finance_debt_payments__payment_id__void_post","parameters":[{"name":"payment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__finance__debt__routes__VoidIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Void Payment Api Finance Debt Payments  Payment Id  Void Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt/reconciliation":{"get":{"tags":["finance"],"summary":"Reconciliation","description":"What Sadia entered through a day, class by class, beside the court\nreport's total for the class (a reference only). Done means every class\nmatches.","operationId":"reconciliation_api_finance_debt_reconciliation_get","parameters":[{"name":"through","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Through"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reconciliation Api Finance Debt Reconciliation Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt/reconciliation/court-report":{"put":{"tags":["finance"],"summary":"Set Court Report","description":"Enter the court report's per-class paid totals through a day. A\nreference column: it is compared with what was entered and never becomes a\npayment (DEBT-01).","operationId":"set_court_report_api_finance_debt_reconciliation_court_report_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtReportIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Set Court Report Api Finance Debt Reconciliation Court Report Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/debt/gate/operating-cash":{"post":{"tags":["finance"],"summary":"Set Operating Cash","description":"Enter the operating cash figure the gate's $750,000 reserve test reads.","operationId":"set_operating_cash_api_finance_debt_gate_operating_cash_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatingCashIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Set Operating Cash Api Finance Debt Gate Operating Cash Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/plaid-webhook":{"post":{"tags":["finance-bank"],"summary":"Plaid Webhook","description":"Plaid says a sync has something new. Verified against Plaid's signing key\nfirst; an unverifiable request is a 401 and does nothing.","operationId":"plaid_webhook_api_finance_bank_plaid_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Plaid Webhook Api Finance Bank Plaid Webhook Post"}}}}}}},"/api/finance/bank/sync-status":{"get":{"tags":["finance-bank"],"summary":"Sync Status","description":"Per connection: when it last synced, how it is doing, how many company\nrows it returned and the earliest date among them; the business accounts.\nNothing about the personal account.","operationId":"sync_status_api_finance_bank_sync_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Sync Status Api Finance Bank Sync Status Get"}}}}}}},"/api/finance/bank/transactions":{"get":{"tags":["finance-bank"],"summary":"List Transactions","description":"The company's bank transactions (the three business accounts): id, date,\npayee, amount, account label, category, matched bill id, source\n(`business`) and status. Company rows only; the personal account is not\nreachable from here (BNK-06).","operationId":"list_transactions_api_finance_bank_transactions_get","parameters":[{"name":"month","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},{"type":"null"}],"title":"Month"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Transactions Api Finance Bank Transactions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/suggestions":{"get":{"tags":["finance-bank"],"summary":"List Suggestions","description":"What Rooster proposes for the company rows it could not settle (a category\nit was unsure of; a debit with no record on Roost's side). Proposals only.","operationId":"list_suggestions_api_finance_bank_suggestions_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Suggestions Api Finance Bank Suggestions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/feed":{"get":{"tags":["finance-bank"],"summary":"Transactions Screen","description":"The Finance Transactions page (`Transactions.dc.html`, Frederick's 2026-10-04\ndesign): outgoing company payments plus items he confirmed as business,\nwith the page's filters, per-category totals and the business feed's state.\n`finance:read`. His personal account never appears: not its name, last four,\nbalance, connection state or any unconfirmed item (BNK-06).","operationId":"transactions_screen_api_finance_bank_feed_get","parameters":[{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date From"}},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date To"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"account","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Account"}},{"name":"needs_look","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Needs Look"}},{"name":"unmatched","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Unmatched"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Transactions Screen Api Finance Bank Feed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/viewer":{"get":{"tags":["finance-bank"],"summary":"Viewer","description":"Is the person looking at Finance Transactions Frederick himself? The page\ndraws the \"Your review\" tab only when this says yes (`Transactions.dc.html`,\n2026-10-04). It grants nothing: the owner-only routes check the principal\nthemselves and answer 404 to everyone else. `finance:read`.","operationId":"viewer_api_finance_bank_viewer_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Viewer Api Finance Bank Viewer Get"}}}}}}},"/api/finance/bank/owner/review":{"get":{"tags":["finance-bank"],"summary":"Owner Review Page","description":"Frederick's private review page (`Personal Review.dc.html`): his account\nline, the flags waiting for him, his rules and where he stands. 404 for\nanyone else, whatever grants they hold (BNK-06).","operationId":"owner_review_page_api_finance_bank_owner_review_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Owner Review Page Api Finance Bank Owner Review Get"}}}}}}},"/api/finance/bank/paid-personally":{"get":{"tags":["finance-bank"],"summary":"Paid Personally","description":"The \"Paid personally by Frederick\" report (BNK-11): the business payments he\nCONFIRMED (date, payee, amount, category, his note, the period it settles in and\nthat period's result, source `paid personally by Frederick`), the company's\nmatched transfers to and from him, and the settlement per month and year to date.\nOpen to whoever holds `finance:read`. It reads two company tables and never the\npersonal account, so no personal account, count, total or unflagged transaction\ncan appear in it.","operationId":"paid_personally_api_finance_bank_paid_personally_get","parameters":[{"name":"year","in":"query","required":false,"schema":{"type":"integer","maximum":2100,"minimum":2020,"default":2026,"title":"Year"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Paid Personally Api Finance Bank Paid Personally Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/paid-personally.csv":{"get":{"tags":["finance-bank"],"summary":"Paid Personally Csv","description":"The same report as a CSV, for the accountant to code from.","operationId":"paid_personally_csv_api_finance_bank_paid_personally_csv_get","parameters":[{"name":"year","in":"query","required":false,"schema":{"type":"integer","maximum":2100,"minimum":2020,"default":2026,"title":"Year"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/paid-personally/print":{"get":{"tags":["finance-bank"],"summary":"Paid Personally Print","description":"The printable view: a static shell with no data. It fetches the report above\nwith the session the browser holds, so only `finance:read` sees anything in it.\nDesign: none, a plain document (`report_page.py`).","operationId":"paid_personally_print_api_finance_bank_paid_personally_print_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/finance/bank/owner/transactions":{"get":{"tags":["finance-bank"],"summary":"Owner Transactions","description":"Frederick's flagged payments, with the fields the company screens use\n(id, date, payee, amount, account label, category, source `paid personally`,\nstatus `suggested | confirmed | personal`). Only what Rooster flagged; never an\nunflagged payment. 404 for anyone else.","operationId":"owner_transactions_api_finance_bank_owner_transactions_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(suggested|confirmed|personal)$"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Owner Transactions Api Finance Bank Owner Transactions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/owner/suggestions":{"get":{"tags":["finance-bank"],"summary":"Owner Suggestions","description":"The flags waiting for his answer: what Rooster thinks is a business\nexpense, why, and its guess at the category. 404 for anyone else.","operationId":"owner_suggestions_api_finance_bank_owner_suggestions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Owner Suggestions Api Finance Bank Owner Suggestions Get"}}}}}}},"/api/finance/bank/owner/suggestions/{flag_id}/answer":{"post":{"tags":["finance-bank"],"summary":"Owner Answer","description":"Answer one flag over the API (the same answer the Slack thread and the\nbuttons give). Idempotent: a flag already answered reports so and changes\nnothing. 404 for anyone but the owner, and for a flag that is not his.","operationId":"owner_answer_api_finance_bank_owner_suggestions__flag_id__answer_post","parameters":[{"name":"flag_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Flag Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Owner Answer Api Finance Bank Owner Suggestions  Flag Id  Answer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/owner/backlog":{"get":{"tags":["finance-bank"],"summary":"Owner Backlog","description":"How the backlog of his personal account stands: how many payments wait for\nhis answer and how many he has settled. The one Today card draws exactly this\n(BNK-13). Counts only. 404 for anyone but the owner.","operationId":"owner_backlog_api_finance_bank_owner_backlog_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"integer"},"type":"object","title":"Response Owner Backlog Api Finance Bank Owner Backlog Get"}}}}}}},"/api/finance/bank/owner/batches/{batch_id}/confirm":{"post":{"tags":["finance-bank"],"summary":"Owner Confirm Batch","description":"\"Confirm all\" on one learned rule's batch (BNK-09, BNK-13): every payment in\nit becomes a company expense he paid personally. The same act as replying\n\"confirm\" in the batch's Slack thread, and claim-once like it, so a tap here\nand a reply there are one answer. 404 for anyone but the owner, and for a\nbatch that is not his.","operationId":"owner_confirm_batch_api_finance_bank_owner_batches__batch_id__confirm_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Owner Confirm Batch Api Finance Bank Owner Batches  Batch Id  Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/owner/batches/{batch_id}/undo":{"post":{"tags":["finance-bank"],"summary":"Owner Undo Batch","description":"\"Undo the rule\" on a batch: the rule is taken back, its waiting matches are\ndropped and the payments are judged again on their own. 404 for anyone but the\nowner, and for a batch that is not his.","operationId":"owner_undo_batch_api_finance_bank_owner_batches__batch_id__undo_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Owner Undo Batch Api Finance Bank Owner Batches  Batch Id  Undo Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/owner/position":{"get":{"tags":["finance-bank"],"summary":"Owner Position","description":"Frederick's own position, month by month and year to date, every leg\nitemized: transfers down (business to him), business payments, transfers up (him\nto the business), his own funds in, and the result of\n`business payments + transfers up - transfers down` (negative is his personal use,\npositive means more business expenses were paid from his account than came in; not a\nclaim). 404 for anyone else.","operationId":"owner_position_api_finance_bank_owner_position_get","parameters":[{"name":"year","in":"query","required":false,"schema":{"type":"integer","maximum":2100,"minimum":2020,"default":2026,"title":"Year"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Owner Position Api Finance Bank Owner Position Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/owner/statements":{"post":{"tags":["finance-bank"],"summary":"Owner Import Statement","description":"Import one statement file (CSV, OFX/QFX or a text PDF) for Personal Checking\n(BNK-10): the raw file is the request body. The answer is counts and dates only.\nImporting a file twice stores nothing twice. 404 for anyone but the owner; 422 for\na file that cannot be read or whose money direction cannot be confirmed (the\nmessage says what to export instead); 413 above 10 MB.","operationId":"owner_import_statement_api_finance_bank_owner_statements_post","parameters":[{"name":"filename","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200,"title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Owner Import Statement Api Finance Bank Owner Statements Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/finance/bank/owner-summary":{"get":{"tags":["finance-bank"],"summary":"Owner Summary","description":"How much of Frederick's own account Roost holds: a row count and the\nearliest and latest dates. Counts and dates only. 404 for anyone else.","operationId":"owner_summary_api_finance_bank_owner_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Owner Summary Api Finance Bank Owner Summary Get"}}}}}}},"/api/finance/bank/link-page":{"get":{"tags":["finance-bank"],"summary":"Link Page Html","description":"The one-button page for linking the bank again (BNK-10). A static shell:\nno data, nothing to protect; the owner-only routes below answer its calls.\nDesign: none, a minimal utility on Frederick's instruction (`link_page.py`).","operationId":"link_page_html_api_finance_bank_link_page_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/finance/bank/link/start":{"post":{"tags":["finance-bank"],"summary":"Link Start","description":"Begin linking the bank again with 730 days of history requested. Returns\nthe page state, including the one URL to open.","operationId":"link_start_api_finance_bank_link_start_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Link Start Api Finance Bank Link Start Post"}}}}}}},"/api/finance/bank/link/status":{"get":{"tags":["finance-bank"],"summary":"Link Status","description":"Advance the open attempt one step and say where it stands: waiting for\nthe bank, waiting for Plaid's history, or done with the earliest date Plaid\nactually returned. Starts a sync of a new connection and keeps nudging it\nwhile history is pending.","operationId":"link_status_api_finance_bank_link_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Link Status Api Finance Bank Link Status Get"}}}}}}},"/api/commissions/me/renewal-summary":{"get":{"tags":["commissions"],"summary":"My Renewal Summary","description":"The \"My Renewal Commission\" card: 2% on eligible year-2+ collections\nfrom the caller's assigned accounts (spec section 8).","operationId":"my_renewal_summary_api_commissions_me_renewal_summary_get","parameters":[{"name":"principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response My Renewal Summary Api Commissions Me Renewal Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commissions/me/renewal-entries":{"get":{"tags":["commissions"],"summary":"My Renewal Entries","description":"The card's click-through: one line per ledger entry.","operationId":"my_renewal_entries_api_commissions_me_renewal_entries_get","parameters":[{"name":"principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response My Renewal Entries Api Commissions Me Renewal Entries Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commissions/me/portfolio-retention":{"get":{"tags":["commissions"],"summary":"My Portfolio Retention","description":"The \"Portfolio Retention Incentive\" card. 404 for anyone who is not\nthe configured recipient -- the card does not exist for them.","operationId":"my_portfolio_retention_api_commissions_me_portfolio_retention_get","parameters":[{"name":"principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response My Portfolio Retention Api Commissions Me Portfolio Retention Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commissions/me/portfolio-retention/events":{"get":{"tags":["commissions"],"summary":"My Portfolio Retention Events","description":"The retention detail view: every trailing-12-month event (pending\nvisible, excluded visible) and the monthly measured rates.","operationId":"my_portfolio_retention_events_api_commissions_me_portfolio_retention_events_get","parameters":[{"name":"principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response My Portfolio Retention Events Api Commissions Me Portfolio Retention Events Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commissions/overrides":{"post":{"tags":["commissions"],"summary":"Record Override","description":"Spec section 12: an administrator corrects one of the policy's\ninputs. The original value is kept, the new value, the user, the time\nand the reason are recorded, and the recalculation the field implies\nruns in the same transaction (`commission_policy.apply_override`).","operationId":"record_override_api_commissions_overrides_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverrideRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Override Api Commissions Overrides Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commissions/retention-events":{"post":{"tags":["commissions"],"summary":"Record Retention Event","description":"Spec section 7: one customer decision point. An EXCLUDED event is\napproved by the administrator recording it (the grant is the approval;\nthe reason is required). An event whose `source_document` is already\non file is not duplicated: the response says so.","operationId":"record_retention_event_api_commissions_retention_events_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionEventRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Retention Event Api Commissions Retention Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commissions/me/potential":{"get":{"tags":["commissions"],"summary":"My Potential","description":"The Today tile(s): one figure (account manager) or two (the\nportfolio recipient also gets her current retention rate and tier).\n404 for anyone who is neither -- the tile does not exist for them,\nsame as `commission_today_routes.py`'s own `/me/portfolio-retention`.","operationId":"my_potential_api_commissions_me_potential_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Potential Api Commissions Me Potential Get"}}}}}}},"/api/fulfillment/facilities":{"get":{"tags":["fulfillment"],"summary":"List Facilities","description":"The facilities a fulfillment screen may name — under fulfillment:read.\n\nISS-122. Sam, a Fulfillment Technician: \"The Shipping Tab doesn't show a\nlist of available facilities, even when typing it out.\" The Shipping\nwizard's step-2 picker fetched GET /api/orgs/facilities, which is gated on\n`orgs:read` — a key no fulfillment role holds. Migration 0053 binds the\ntechnician role exactly `fulfillment:read` + `fulfillment:write`, and\ndocs/01_decisions/ROOST_V2_ROLE_FUNCTION_MAPPING.md gives the Fulfillment\nbundle no `orgs`/`facilities` grant at all. So every technician got a 403,\nuseFacilities.ts swallowed it as a best-effort enrichment failure, and the\npicker stayed empty forever — for the only people who work that page.\nTyping changed nothing, because the filter runs over an empty list.\n\nA manager holding `orgs:read` saw a full list at the same time, which is\nhow ISS-64 (\"all facilities are showing in the List\") and this could both\nbe true of one dropdown.\n\nSame move migration 0150 made for parcel search: the key that opens a\nscreen is that screen's own key, never a second module's. Widening\n`orgs:read` onto the floor would hand the warehouse every organization,\ncontact and contract to fix a dropdown.\n\n\"Contracted\" is still decided in exactly one place — orgs/service.py's\nlist_facilities, off BILLING_ELIGIBLE_STAGES plus a facility code — and\nnothing is re-derived here.","operationId":"list_facilities_api_fulfillment_facilities_get","parameters":[{"name":"contracted_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Contracted Only"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Facilities Api Fulfillment Facilities Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/packaging":{"get":{"tags":["fulfillment"],"summary":"List Packaging","description":"The one packaging catalogue -- what the wizard's box-size picker reads\ninstead of its own hard-coded const (migration 0553; ScanToShipWizardPage\nused to carry a second copy of print/postage_service.FLAT_RATE_BOXES).\n\n`bookable` is on every row so the wizard can tell \"we stock it\" from \"we\ncan buy postage for it\" without duplicating that judgement client-side --\n`envelope` comes back with `bookable: false` and no `sera_packaging_type`.","operationId":"list_packaging_api_fulfillment_packaging_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Packaging Api Fulfillment Packaging Get"}}}}}}},"/api/fulfillment/packaging/barcodes":{"get":{"tags":["fulfillment"],"summary":"List Packaging Barcodes","description":"Every barcode bound to a packaging stock item -- what the wizard's\nparcel-step scan classifier checks BEFORE its own \">10 alphanumeric\ncharacters is a tracking number\" rule (ScanToShipWizardPage.tsx\n`classifyScan`). Without this, a 12-digit box barcode scanned on the\nparcel step reads as a tracking number and jumps straight to Review --\nthe bug this endpoint exists to close.","operationId":"list_packaging_barcodes_api_fulfillment_packaging_barcodes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Packaging Barcodes Api Fulfillment Packaging Barcodes Get"}}}}}}},"/api/fulfillment/wizard/open-box":{"post":{"tags":["fulfillment"],"summary":"Open Box","operationId":"open_box_api_fulfillment_wizard_open_box_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenBoxRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Open Box Api Fulfillment Wizard Open Box Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/open-boxes":{"get":{"tags":["fulfillment"],"summary":"List Open Boxes","description":"Every open box on the floor, for the manager panel.\n\n/wizard/my-open-box answers \"what am I working on\"; this answers \"what is\nopen right now\", which is a different question and the one a manager asks\nbefore a shift ends. A box open since Jul 23 with 89 parcels in it is\nsomebody's forgotten work, and nothing surfaced it.","operationId":"list_open_boxes_api_fulfillment_wizard_open_boxes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Open Boxes Api Fulfillment Wizard Open Boxes Get"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}":{"get":{"tags":["fulfillment"],"summary":"Get Box","description":"One box by id, so a manager can open somebody else's.\n\nOpen Boxes → Continue lands here. Read-only and gated on\nfulfillment:packaging:read; the write paths (scan, close, void) do their own\nowner-or-manager check, so seeing a box is not the same as being able to\nact on it.","operationId":"get_box_api_fulfillment_wizard_boxes__shipment_id__get","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Box Api Fulfillment Wizard Boxes  Shipment Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/my-open-box":{"get":{"tags":["fulfillment"],"summary":"Get My Open Box","operationId":"get_my_open_box_api_fulfillment_wizard_my_open_box_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Response Get My Open Box Api Fulfillment Wizard My Open Box Get"}}}}}}},"/api/fulfillment/wizard/next-facility":{"get":{"tags":["fulfillment"],"summary":"Get Next Facility","description":"Which facility Rooster says to ship next, who is after it and who is waiting\n(issue #4503 slice 7, FLR-109 item 5; the screen is `Boxing Facility Step.dc.html`).\n\nFrederick, 2026-10-12: \"I think let's try it with rooster's pick for now.\" While\nthe setting `print_rooster_feed` is on, the Facility step shows this and nothing\nelse to choose from. With it off (the default, and a missing row) this answers\n`{\"enabled\": false}` without reading anything and the step keeps today's picker.\nREAD ONLY: it opens no box; `POST /wizard/open-box` is the one writer and the\none-open-box-per-person rule (`OneOpenBoxExists`) is unchanged.","operationId":"get_next_facility_api_fulfillment_wizard_next_facility_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Next Facility Api Fulfillment Wizard Next Facility Get"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/leftover-count":{"get":{"tags":["fulfillment"],"summary":"Leftover Count","description":"How many other parcels for this box's own facility are PACKED AND\nWAITING (printed and packaged, in no box), so the station can show \"12\nmore for Sonoma are packed and waiting: add them if you have them\" before\nDone. Information only since 2026-09-24 (Frederick): nothing refuses a\nclose over it. See `service.parcels_left_behind_count`.","operationId":"leftover_count_api_fulfillment_wizard_boxes__shipment_id__leftover_count_get","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Leftover Count Api Fulfillment Wizard Boxes  Shipment Id  Leftover Count Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/scan":{"post":{"tags":["fulfillment"],"summary":"Scan Parcel","operationId":"scan_parcel_api_fulfillment_wizard_boxes__shipment_id__scan_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanParcelRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Scan Parcel Api Fulfillment Wizard Boxes  Shipment Id  Scan Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/packaging/instruct":{"post":{"tags":["fulfillment"],"summary":"Instruct Packaging","description":"\"Grab a MEDIUM -- 41 parcels...\" Rooster names the packaging BEFORE\nany parcel is scanned into this box, from the facility's own waiting\ncount. Idempotent: a resumed box gets back the SAME instruction it was\ngiven the first time, per service.instruct_packaging.","operationId":"instruct_packaging_api_fulfillment_wizard_boxes__shipment_id__packaging_instruct_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Instruct Packaging Api Fulfillment Wizard Boxes  Shipment Id  Packaging Instruct Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/packaging/scan":{"post":{"tags":["fulfillment"],"summary":"Scan Packaging","description":"The barcode on the box the technician actually picked up.\n\n⛔NEVER BLOCKS: a match writes and returns in this one call (no follow-up\nclick needed to advance); a mismatch classifies without writing, and the\nweb wizard's \"Use this one\" continues through\nPOST .../packaging/confirm-mismatch; an unrecognised barcode is recorded\nimmediately and the box still proceeds either way.","operationId":"scan_packaging_api_fulfillment_wizard_boxes__shipment_id__packaging_scan_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackagingScanRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Scan Packaging Api Fulfillment Wizard Boxes  Shipment Id  Packaging Scan Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/packaging/bind-barcode":{"post":{"tags":["fulfillment"],"summary":"Bind Box Barcode At The Station","description":"\"This box's barcode isn't in Roost yet. Which box is it?\" -- the packer's\nanswer binds the barcode for good and scans the box again (2026-09-24;\nsee service.bind_box_barcode_at_the_station). Gated on the packing\nstation's own grant, not Inventory's: any packer can do it, and the\ndepartment head is shown every binding.","operationId":"bind_box_barcode_at_the_station_api_fulfillment_wizard_boxes__shipment_id__packaging_bind_barcode_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BindBoxBarcodeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Bind Box Barcode At The Station Api Fulfillment Wizard Boxes  Shipment Id  Packaging Bind Barcode Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/packaging/confirm-mismatch":{"post":{"tags":["fulfillment"],"summary":"Confirm Packaging Mismatch","description":"\"Use this one\" -- continues with the packaging actually in the\ntechnician's hands, over what Rooster instructed. ⛔THE SHELF MAY\nGENUINELY BE EMPTY: this never refuses the box, only records why the\nscan disagreed.","operationId":"confirm_packaging_mismatch_api_fulfillment_wizard_boxes__shipment_id__packaging_confirm_mismatch_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmPackagingMismatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Confirm Packaging Mismatch Api Fulfillment Wizard Boxes  Shipment Id  Packaging Confirm Mismatch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/wizard/boxes/{shipment_id}/void":{"post":{"tags":["fulfillment"],"summary":"Void Box","operationId":"void_box_api_fulfillment_wizard_boxes__shipment_id__void_post","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Void Box Api Fulfillment Wizard Boxes  Shipment Id  Void Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcels":{"get":{"tags":["fulfillment"],"summary":"List Parcels","operationId":"list_parcels_api_fulfillment_parcels_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"facility_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"shipment_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Shipment Id"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Parcels Api Fulfillment Parcels Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/search":{"get":{"tags":["fulfillment"],"summary":"Search","description":"One field. Both result sets. Never a scope.\n\n07 §1: \"There is ONE search field. No scope tabs, no 'search parcels' vs\n'search boxes' toggle, no advanced-filter drawer. The user types anything\nand the backend figures out what it is.\"","operationId":"search_api_fulfillment_search_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Search Api Fulfillment Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/boxes/by-tracking/{tracking_number}/parcels":{"get":{"tags":["fulfillment"],"summary":"Box Manifest By Tracking","description":"A box's manifest, reached the way the search result identifies it.\n\n07 §5. `by-tracking` sits in the path rather than replacing\n/boxes/{shipment_id}/parcels outright because the print and packaging\nscreens still hold shipment UUIDs; the two coexist and mean the same\nthing from different directions.\n\nGated on `fulfillment:parcels:read` alongside /search, not on\nfulfillment:read, because it is the same screen: 07 §6's expanded box row\nIS this call, and a search that returns a box you cannot open answers\n\"where is this parcel\" with half a sentence. The sibling route below,\n/boxes/{shipment_id}/parcels, keeps fulfillment:read — it is reached from\nthe print and packaging stations, which are warehouse surfaces.\n\n`q` is the search term the box was opened from, when it was: the parcels\nthat match it lead the manifest, so the row the box record tints is on\nthe first page of a 171-parcel box rather than the fourth (ISS-592).","operationId":"box_manifest_by_tracking_api_fulfillment_boxes_by_tracking__tracking_number__parcels_get","parameters":[{"name":"tracking_number","in":"path","required":true,"schema":{"type":"string","title":"Tracking Number"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Box Manifest By Tracking Api Fulfillment Boxes By Tracking  Tracking Number  Parcels Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/boxes/{shipment_id}/parcels":{"get":{"tags":["fulfillment"],"summary":"Get Box Manifest","description":"The parcels in one box.\n\nBehind the lookup's box row, which the design makes clickable so the\nchain does not dead-end: \"clicking a box opens THAT box's parcel\nmanifest.\"","operationId":"get_box_manifest_api_fulfillment_boxes__shipment_id__parcels_get","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Shipment Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Box Manifest Api Fulfillment Boxes  Shipment Id  Parcels Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/capacity":{"get":{"tags":["fulfillment"],"summary":"Get Capacity","description":"Capacity.dc.html: \"People, equipment, labor, and turnaround. Read\nonly.\" No page grant of its own (lib/subNav.ts's Capacity item carries\nno `grant`: anyone who has already unlocked Fulfillment through any of\nits other pages sees it, matching the section's own \"no grant here\nmeans the section decides\" rule) -- gated on `permissions.FLOOR`, the\nsame \"any fulfillment read grant\" membership check `/facilities`\nalready uses.","operationId":"get_capacity_api_fulfillment_capacity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Capacity Api Fulfillment Capacity Get"}}}}}}},"/api/fulfillment/sla/facilities":{"get":{"tags":["fulfillment"],"summary":"Get Facility Sla","description":"Defaults to the trailing 30 days (design bundle: 'Target is 24h\nship' — a rolling window is the useful default, not an all-time one\nthat would dilute a facility's current performance with ancient\nhistory).","operationId":"get_facility_sla_api_fulfillment_sla_facilities_get","parameters":[{"name":"period_start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Period Start"}},{"name":"period_end_exclusive","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Period End Exclusive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Facility Sla Api Fulfillment Sla Facilities Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/never-shipped":{"get":{"tags":["fulfillment"],"summary":"Get Never Shipped","description":"The reprint list: transcribed parcels with no evidence they ever left\nthe building, past `settings.never_shipped_after_hours` (94 h to start).\nFLOOR-GPS-SCHEDULER-001, #2981.\n\nFrederick, 2026-10-03: \"Rooster sees something that was transcribed, but\nit never was shipped out the building. That's true work that we have to\nidentify and have reprinted somehow ... we'll probably end up just\nreprinting that once the Centrix cutover is done.\" A manager's read, the\nsame grants as the SLA page (a promise missed is a promise missed). The\nreprint itself is not here: it is a repair, run by the manager at the\ncutover (`tools/repairs/reprint_the_parcels_that_never_shipped.py`).\n\n`summary` is the count, the oldest age and the open parcels by intake\nweek; `rows` are the parcels oldest first. Rooster reads the same list\n(`manager_tools.read_never_shipped`) and files the\n`parcels_transcribed_never_shipped` finding from it.","operationId":"get_never_shipped_api_fulfillment_never_shipped_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"facility_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Never Shipped Api Fulfillment Never Shipped Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/analytics":{"get":{"tags":["fulfillment"],"summary":"Get Analytics","description":"The five steps, one series, computed server-side.\n\n⛔`day` REPLACES THE PERIOD RATHER THAN NARROWING IT. Clicking a bar in\nthe chart scopes the WHOLE page to that day -- the spec is explicit that\na subhead which changes while the numbers do not is worse than no claim\nat all -- so the day, when given, IS the window.\n\n`range` defaults to month-to-date, which is the period control's own\ndefault. `30` remains valid and is what the chart always covers.","operationId":"get_analytics_api_fulfillment_analytics_get","parameters":[{"name":"range","in":"query","required":false,"schema":{"type":"string","default":"mtd","title":"Range"}},{"name":"day","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Day"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Analytics Api Fulfillment Analytics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcels/{parcel_id}":{"get":{"tags":["fulfillment"],"summary":"Get Parcel","operationId":"get_parcel_api_fulfillment_parcels__parcel_id__get","parameters":[{"name":"parcel_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Parcel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Parcel Api Fulfillment Parcels  Parcel Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcels/{parcel_id}/address-changed":{"post":{"tags":["fulfillment"],"summary":"Flag Address Changed","operationId":"flag_address_changed_api_fulfillment_parcels__parcel_id__address_changed_post","parameters":[{"name":"parcel_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Parcel Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagAddressChangedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Flag Address Changed Api Fulfillment Parcels  Parcel Id  Address Changed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcels/{parcel_id}/address-changed/resolve":{"post":{"tags":["fulfillment"],"summary":"Resolve Address Change Hold","operationId":"resolve_address_change_hold_api_fulfillment_parcels__parcel_id__address_changed_resolve_post","parameters":[{"name":"parcel_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Parcel Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resolve Address Change Hold Api Fulfillment Parcels  Parcel Id  Address Changed Resolve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items":{"post":{"tags":["fulfillment"],"summary":"Create Inventory Item","operationId":"create_inventory_item_api_fulfillment_inventory_items_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInventoryItemRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Inventory Item Api Fulfillment Inventory Items Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["fulfillment"],"summary":"List Inventory Items","operationId":"list_inventory_items_api_fulfillment_inventory_items_get","parameters":[{"name":"item_group","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Item Group"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Inventory Items Api Fulfillment Inventory Items Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items/{item_id}":{"get":{"tags":["fulfillment"],"summary":"Get Inventory Item","operationId":"get_inventory_item_api_fulfillment_inventory_items__item_id__get","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Inventory Item Api Fulfillment Inventory Items  Item Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items/{item_id}/bind-upc":{"post":{"tags":["fulfillment"],"summary":"Bind Packaging Barcode","description":"A manager binds an UNRECOGNISED box-scan barcode to a stock item, from\nthe Inventory panel.\n\n⛔THE ONLY ROUTE IN THIS CODEBASE THAT MAY WRITE inventory_items.upc FROM\nTHE BOX-SCAN FLOW -- see service.bind_packaging_barcode. Gated on\n`fulfillment:inventory:write`, the manager grant for this panel, not the\nbox wizard's own grant: binding is a catalogue decision, not a floor act.","operationId":"bind_packaging_barcode_api_fulfillment_inventory_items__item_id__bind_upc_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BindPackagingBarcodeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Bind Packaging Barcode Api Fulfillment Inventory Items  Item Id  Bind Upc Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items/{item_id}/transactions":{"post":{"tags":["fulfillment"],"summary":"Record Inventory Transaction","operationId":"record_inventory_transaction_api_fulfillment_inventory_items__item_id__transactions_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordTransactionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Inventory Transaction Api Fulfillment Inventory Items  Item Id  Transactions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items/{item_id}/count":{"post":{"tags":["fulfillment"],"summary":"Set Inventory Count","description":"\"Set count\" on the Inventory page: the person enters the number on\nthe shelf and Roost writes the difference as a count transaction,\nrecording who and when (Frederick, 2026-09-24: Samuel \"can enter the\ntotal number in stock\"). Gated on `fulfillment:inventory:write`, the\nsame grant as every other stock change on that page -- the Fulfillment\ntechnician role holds it in production (checked read-only 2026-09-24).","operationId":"set_inventory_count_api_fulfillment_inventory_items__item_id__count_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCountRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Inventory Count Api Fulfillment Inventory Items  Item Id  Count Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items/{item_id}/supply-sources":{"get":{"tags":["fulfillment"],"summary":"List Supply Sources","operationId":"list_supply_sources_api_fulfillment_inventory_items__item_id__supply_sources_get","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Supply Sources Api Fulfillment Inventory Items  Item Id  Supply Sources Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["fulfillment"],"summary":"Create Supply Source","operationId":"create_supply_source_api_fulfillment_inventory_items__item_id__supply_sources_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplySourceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Supply Source Api Fulfillment Inventory Items  Item Id  Supply Sources Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/inventory/items/{item_id}/supply-sources/{source_id}/prefer":{"post":{"tags":["fulfillment"],"summary":"Prefer Supply Source","operationId":"prefer_supply_source_api_fulfillment_inventory_items__item_id__supply_sources__source_id__prefer_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Prefer Supply Source Api Fulfillment Inventory Items  Item Id  Supply Sources  Source Id  Prefer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcel-view/{ref}":{"get":{"tags":["fulfillment"],"summary":"View Parcel","description":"Open the viewer: how many pages, what to call the download, or the\nsentence explaining there is no scan (an app-made photos/letters/cards\norder). Audited as `opened` once the scan has actually been reached.","operationId":"view_parcel_api_fulfillment_parcel_view__ref__get","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response View Parcel Api Fulfillment Parcel View  Ref  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcel-view/{ref}/pages/{page}":{"get":{"tags":["fulfillment"],"summary":"View Parcel Page","description":"One page of the scan, the envelope being page 1. Audited as `page`\nwith the page number, so the record says which sheets were looked at.","operationId":"view_parcel_page_api_fulfillment_parcel_view__ref__pages__page__get","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string","title":"Ref"}},{"name":"page","in":"path","required":true,"schema":{"type":"integer","title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/parcel-view/{ref}/file":{"get":{"tags":["fulfillment"],"summary":"Download Parcel Scan","description":"The whole scan as the mail service holds it -- the PDF -- for the\nviewer's Download and Print. Streamed through, never written anywhere.\nAudited as `downloaded`.","operationId":"download_parcel_scan_api_fulfillment_parcel_view__ref__file_get","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string","title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/floor-timing/summary":{"get":{"tags":["fulfillment"],"summary":"Get Summary","description":"Defaults to the trailing 7 days -- long enough to show a real\npacking-pace and box-timing picture, short enough that the gap query\nstays cheap. Both ends are UTC instants; a caller (the web page, or\nRooster's own read later) that wants the floor's own calendar day\nresolves it before calling, same as `fulfillment.routes.get_analytics`\nalready does for its own `day`/`start`/`end` params.","operationId":"get_summary_api_fulfillment_floor_timing_summary_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"}},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Until"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Summary Api Fulfillment Floor Timing Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/hr/schedule":{"get":{"tags":["floor"],"summary":"Get Schedule","description":"Coverage against demand, the blocks table, the surge strip\n(`dept=fulfillment`) or the CS roster grid (`dept=support`) -- U3\n(`floor_blocks.routes.fulfillment_schedule_payload`/`cs_schedule_\npayload`).","operationId":"get_schedule_api_floor_hr_schedule_get","parameters":[{"name":"dept","in":"query","required":false,"schema":{"type":"string","pattern":"^(fulfillment|support)$","default":"fulfillment","title":"Dept"}},{"name":"week","in":"query","required":false,"schema":{"type":"string","pattern":"^(this|next)$","default":"this","title":"Week"}},{"name":"step","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Step"}},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Schedule Api Floor Hr Schedule Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/hr/manager":{"get":{"tags":["floor"],"summary":"Get Manager","description":"Human Resources > Manager -- Rooster's live view of staffing, people\nexceptions and management work (`HR_HANDOFF/README.md` §4, Track E).\n\n⛔THE COMPOSITION ROOT FOR THE MANAGER READ MODEL, AND THE ONLY PLACE\nTHAT KNOWS BOTH HALVES. `workforce.manager_view` is read-only domain and\ndeliberately does not import `floor_blocks`; the have-of-need instrument\nit needs for Staffing health is the SAME payload the Schedule tab reads,\nbuilt here and handed in, so there is no second coverage computation.\n\n⛔AND IT IS THE ONLY DEFINITION OF THIS PATH. Two builds arrived at\n`GET /api/floor/hr/manager` independently and for a while both were\nregistered; FastAPI answers with whichever was imported first, so the\nsecond was a route nothing could reach and no gate reads a duplicate\npath as an error. The two read models are BOTH kept and BOTH reached\nfrom here, because they answer different questions:\n\n  * `workforce.manager_view.board` is the BOARD -- open management work,\n    people needing attention, staffing health, coaching, requests,\n    per-work activity, and the policy register.\n  * `rooster.executive.manager_timeline.manager_payload` is the\n    management COMMUNICATIONS timeline -- one UNION over `cover_asks`,\n    `cover_responses`, `announcement_recipients`, `escalations` and\n    `employee_requests`, ordered by the clock and limited ONCE, with one\n    sentence per event composed API-side. ⛔Six reads each limited to N\n    and merged afterwards is a window with holes in it that read as\n    quiet periods, which is why it stays one statement and is not folded\n    into the board's per-work `events`.\n\n⛔A RECORDING IS NAMED AND NEVER RETURNED. Timeline events carry\n`detail_kind`/`detail_ref`; fetching the thing they name is a separate,\nseparately permissioned read that this route does not perform.\n\n⛔GATED `hr:schedule:read`, THE SAME GRANT `/hr/schedule` ALREADY HOLDS.\nManager is the section's landing page and Staffing & Schedule is one of\nits tabs; a second grant for the same data would be a permission that\nexists only because a screen was renamed.\n\n⛔AND THAT IS A NARROWING FOR ONE POPULATION, SAID OUT LOUD RATHER THAN\nDISCOVERED. The timeline half used to be gated `hr:contractors:read`.\nFolding it in under the board's grant means a principal holding ONLY\n`hr:contractors:read` no longer reads this route: the HR section still\nunlocks for them (`web/src/lib/access.ts` SECTION_FUNCTION_KEYS.hr\naccepts either key) and Contractors and Onboarding still open, but the\nsection's landing page refuses them at its ProbeGate. That follows from\nHR_HANDOFF/README.md §1 making Manager the section's entry point on the\nschedule grant, and it is Frederick's sitemap decision rather than one\ntaken here -- recorded so nobody has to rediscover it from a 403.\n\n⛔NO WRITE LIVES HERE AND NONE MAY. README §11: every decision is a card\non Frederick's Today. An escalated item carries one link and no control.","operationId":"get_manager_api_floor_hr_manager_get","parameters":[{"name":"timeline_limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Timeline Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Manager Api Floor Hr Manager Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/equipment":{"get":{"tags":["floor"],"summary":"Get Equipment","description":"The machine register. `app.modules.equipment.service.list_equipment`\nis the real body (U6): the header figures (count, cost to run, up/down/\ndegraded) plus one card per real machine, computed fresh off\n`equipment`, `equipment_meter` and `equipment_events` (0449, on top of\n0404's original register) on every call -- never cached, never\nhand-set.","operationId":"get_equipment_api_floor_equipment_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Equipment Api Floor Equipment Get"}}}}}}},"/api/floor/contractor":{"get":{"tags":["floor"],"summary":"Get Contractor App","description":"The contractor's own phone app: standing, shifts, availability,\nscorecard. U3 fills shifts + availability (`floor_blocks.service.\ncontractor_shifts_view`/`contractor_availability_view`); \"My numbers\"\n(Scorecard) is U5's own table (`floor_blocks.routes.get_my_numbers`'s own\nbody, `app.modules.workforce.performance.scorecard_for_contractor`).\nEveryone without `floor:contractor` gets a 403 here, never a page with\nnothing on it standing in for a lock. The page itself calls `GET\n/api/floor/me/numbers` directly rather than reading this field -- it is\ncarried here too only so this one aggregated probe stays a true account\nof what the app shows.","operationId":"get_contractor_app_api_floor_contractor_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Contractor App Api Floor Contractor Get"}}}}}}},"/api/floor/workstation":{"get":{"tags":["floor"],"summary":"Get Workstation","description":"The workstation clock-in screen's own probe, for a floor contractor\nor an on-site employee (`permissions.FLOOR_CLOCK`). U4's real body\n(`floor_blocks.clock.workstation_status_payload`): the office-network/\nregistered-device check, which accepted block (if any) is next, the\nintake tub gate, and the clocked-in card's own facts -- same \"replace\nthe body, never the route or its grant\" pattern U3 used above.","operationId":"get_workstation_api_floor_workstation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Workstation Api Floor Workstation Get"}}}}}}},"/api/repairs":{"post":{"tags":["repairs"],"summary":"Request Repair","operationId":"request_repair_api_repairs_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestRepairBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Request Repair Api Repairs Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["repairs"],"summary":"List Repairs","operationId":"list_repairs_api_repairs_get","parameters":[{"name":"mine","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Mine"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Repairs Api Repairs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/repairs/catalog":{"get":{"tags":["repairs"],"summary":"Repair Catalog","description":"Every repair this deployment can run, from the registry's own walk.\n\n⛔WITHOUT THIS THERE IS NO DOOR. A repair module lands in `tools/repairs/`\ngreen and merged, and the only way to ask for it is to POST this router by\nhand with an agent's bearer token -- so a repair Frederick approved the\nidea of can sit in the codebase indefinitely while the thing it fixes\nstays broken. It happened twice in one day: the overseas onboarding and\nthe duplicate issue rows.\n\nThe approval gate is untouched. This lists what CAN be asked for; asking\nstill runs `plan()` read-only against production and still raises the card\nthat only `production:data_repair:approve` can settle.\n\nBehind SUBMIT_DATA_REPAIR rather than open, because the list of things\nthat can rewrite production data is itself a map of where the soft spots\nare -- and it is exactly the set of people who may ask.\n\n`name` is the module's own `NAME`, which is what `POST /api/repairs` takes\nas `repair_name`: one fact, so a screen cannot offer a name the router\nwould refuse.","operationId":"repair_catalog_api_repairs_catalog_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Repair Catalog Api Repairs Catalog Get"}}}}}}},"/api/repairs/{repair_id}":{"get":{"tags":["repairs"],"summary":"Get Repair","operationId":"get_repair_api_repairs__repair_id__get","parameters":[{"name":"repair_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Repair Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Repair Api Repairs  Repair Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/repairs/{repair_id}/approve":{"post":{"tags":["repairs"],"summary":"Approve Repair","operationId":"approve_repair_api_repairs__repair_id__approve_post","parameters":[{"name":"repair_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Repair Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Repair Api Repairs  Repair Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/repairs/{repair_id}/decline":{"post":{"tags":["repairs"],"summary":"Decline Repair","operationId":"decline_repair_api_repairs__repair_id__decline_post","parameters":[{"name":"repair_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Repair Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeclineRepairBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decline Repair Api Repairs  Repair Id  Decline Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment":{"get":{"tags":["equipment"],"summary":"List Equipment Route","operationId":"list_equipment_route_api_equipment_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Equipment Route Api Equipment Get"}}}}}},"post":{"tags":["equipment"],"summary":"Create Equipment Route","operationId":"create_equipment_route_api_equipment_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEquipmentRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Equipment Route Api Equipment Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/prints":{"put":{"tags":["equipment"],"summary":"Set Product Type Route","description":"Say what an existing machine prints (labels, envelopes, flats or\nphotos), so a printer added before the form asked can be corrected on\nits own record. A bad value is a 422, a missing machine a 404.","operationId":"set_product_type_route_api_equipment__equipment_id__prints_put","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetProductTypeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Product Type Route Api Equipment  Equipment Id  Prints Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}":{"get":{"tags":["equipment"],"summary":"Get Equipment Route","operationId":"get_equipment_route_api_equipment__equipment_id__get","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Equipment Route Api Equipment  Equipment Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/history":{"get":{"tags":["equipment"],"summary":"Equipment History Route","description":"\"Full history\" -- the ONLY place the full timeline, meter and uptime\nlive (README section 6).","operationId":"equipment_history_route_api_equipment__equipment_id__history_get","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Equipment History Route Api Equipment  Equipment Id  History Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/label":{"get":{"tags":["equipment"],"summary":"Equipment Label Route","operationId":"equipment_label_route_api_equipment__equipment_id__label_get","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Equipment Label Route Api Equipment  Equipment Id  Label Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/service-document":{"post":{"tags":["equipment"],"summary":"Service Document Route","description":"README section 6: \"Accept a service record without a document\" is\nthe first thing this route must never do -- `file` is optional only so\na missing upload reads as a named 422 (`EquipmentServiceDocumentMissing`)\nrather than FastAPI's generic body-validation error.","operationId":"service_document_route_api_equipment__equipment_id__service_document_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_service_document_route_api_equipment__equipment_id__service_document_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Service Document Route Api Equipment  Equipment Id  Service Document Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/meter":{"put":{"tags":["equipment"],"summary":"Override Meter Route","operationId":"override_meter_route_api_equipment__equipment_id__meter_put","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeterOverrideRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Override Meter Route Api Equipment  Equipment Id  Meter Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/meter/volume":{"get":{"tags":["equipment"],"summary":"Fleet Meter Volume Route","description":"Every machine's monthly page volume at once (Frederick, 2026-09-30):\nthe read Rooster/cost-per-page analysis uses for \"how much did we\nprint last month\" without walking each machine's own page first.\ndocs/runbooks/RICOH_METER_COUNTERS.md.","operationId":"fleet_meter_volume_route_api_equipment_meter_volume_get","parameters":[{"name":"month","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM, default current month","title":"Month"},"description":"YYYY-MM, default current month"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Fleet Meter Volume Route Api Equipment Meter Volume Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/meter/volume":{"get":{"tags":["equipment"],"summary":"Equipment Meter Volume Route","description":"One machine's total/color/black-and-white page volume for one\nmonth -- the same numbers `equipment.service.equipment_card`'s own\n`meter_volume` field carries for this month/last month, available here\nfor any month with history.","operationId":"equipment_meter_volume_route_api_equipment__equipment_id__meter_volume_get","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}},{"name":"month","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM, default current month","title":"Month"},"description":"YYYY-MM, default current month"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Equipment Meter Volume Route Api Equipment  Equipment Id  Meter Volume Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/meter/daily":{"get":{"tags":["equipment"],"summary":"Equipment Meter Daily Route","description":"\"Pages per day\" for the equipment page's Volume panel (Frederick,\n2026-09-30) -- each day's total/color/black-and-white delta, or\n`null`/`\"split_known\": false` where the device's own counters could not\nresolve a split that day, shown honestly rather than as zero.","operationId":"equipment_meter_daily_route_api_equipment__equipment_id__meter_daily_get","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}},{"name":"month","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM, default current month","title":"Month"},"description":"YYYY-MM, default current month"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Equipment Meter Daily Route Api Equipment  Equipment Id  Meter Daily Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/outage/expected-repair":{"put":{"tags":["equipment"],"summary":"Set Expected Repair Route","description":"William or a manager naming when a technician is expected (\"tech\ncoming tomorrow\") -- see `equipment.telemetry.set_expected_repair_at`.\nUpgrades ANY open outage to `fault_class = 'technician'` (F8: booking a\nrepair date implies one is coming, whatever fault class the outage\nopened as). 404 when there is no OPEN outage on this machine at all to\nbound.","operationId":"set_expected_repair_route_api_equipment__equipment_id__outage_expected_repair_put","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedRepairRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Expected Repair Route Api Equipment  Equipment Id  Outage Expected Repair Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/mark-down":{"post":{"tags":["equipment"],"summary":"Mark Down Route","description":"The one-click \"Mark it down\" the Equipment page's telemetry-mismatch\nbanner offers (bug #4, 2026-09-30, PGN-RI-01/SC501-12): the machine's\nown last telemetry read showed active faults and then the link went\nquiet, but shadow mode had suppressed the DM and nobody had filed an\nissue -- `status` stayed 'up' with nothing on the page saying why.\nFiles the repair exactly the way a manager typing it into chat would\n(`repair_dispatch.dispatch_repair`), never a second write path -- the\ntitle is built from the SAME last telemetry snapshot the banner reads\n(`repair_dispatch._fault_text`), and reports the existing ticket\ninstead of a second one if somebody already filed it first.","operationId":"mark_down_route_api_equipment__equipment_id__mark_down_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Down Route Api Equipment  Equipment Id  Mark Down Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/outage/technician-required":{"post":{"tags":["equipment"],"summary":"Technician Required Route","description":"A floor manager says this machine needs a technician (issue #2958):\n\"How can the floor fix that fault? William said he needed a\ntechnician.\" Records the statement, overrides the classifier to\n`fault_class = 'technician'` (opening the outage first when none is\nopen), bounds the multi-day shift re-plan when `expected_repair_at` is\ngiven, and fires the technician escalation (an email to the vendor's\nservice contact, or a card for William) exactly once per outage. Never\na second insert shape, never a second escalation -- see `equipment.\ntechnician_override.record_technician_required`'s own docstring.","operationId":"technician_required_route_api_equipment__equipment_id__outage_technician_required_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnicianRequiredRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Technician Required Route Api Equipment  Equipment Id  Outage Technician Required Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/paperwork/read":{"post":{"tags":["equipment"],"summary":"Read Paperwork Prefill Route","description":"Prefill, before the record exists -- \"Rooster fills what it can\nbefore you type.\" Reads only; the web re-sends the same file to\n`POST /{id}/paperwork` once the machine is saved so it is stored\nagainst the new record.","operationId":"read_paperwork_prefill_route_api_equipment_paperwork_read_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_read_paperwork_prefill_route_api_equipment_paperwork_read_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Read Paperwork Prefill Route Api Equipment Paperwork Read Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/paperwork":{"post":{"tags":["equipment"],"summary":"Apply Paperwork Route","description":"The manual, the purchase invoice, or the vendor contract dropped on\nthe add form -- stores the file, reads it, and writes column 2\n(README.md \"Rooster fills\").","operationId":"apply_paperwork_route_api_equipment__equipment_id__paperwork_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_apply_paperwork_route_api_equipment__equipment_id__paperwork_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Apply Paperwork Route Api Equipment  Equipment Id  Paperwork Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/parts-replaced":{"post":{"tags":["equipment"],"summary":"Record Part Replaced Route","description":"A part replaced by hand, off the floor -- no document to parse, so\nthis is the direct-entry twin of `service_document_route`'s parsed\npath. Both write the SAME `equipment_events` kind='part_replaced' row\nshape (`service_manual.record_part_replacement`).","operationId":"record_part_replaced_route_api_equipment__equipment_id__parts_replaced_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartReplacedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Part Replaced Route Api Equipment  Equipment Id  Parts Replaced Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/throughput":{"put":{"tags":["equipment"],"summary":"Override Throughput Route","description":"\"Change\" on the record's Rated throughput row -- the design's\n\"recorded with who and why\", mirroring `PUT /{id}/meter`'s own shape.","operationId":"override_throughput_route_api_equipment__equipment_id__throughput_put","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThroughputOverrideRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Override Throughput Route Api Equipment  Equipment Id  Throughput Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/pm/list":{"get":{"tags":["equipment"],"summary":"Pm List Route","operationId":"pm_list_route_api_equipment_pm_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pm List Route Api Equipment Pm List Get"}}}}}}},"/api/equipment/issue-conversation":{"post":{"tags":["equipment"],"summary":"Start Equipment Issue Conversation Route","description":"\"File an issue on {label_id}\" -- opens the SAME Issues intake\nconversation `POST /api/issues/intake/converse` continues (a plain\n`conversation_id`, matching that route's own contract exactly), seeded\nwith the equipment context card and Rooster's opening line via\n`intake_service.open_conversation`'s `equipment_label_id` hook. Lives\nhere, not in `app/modules/issues/routes.py`, so U6 never has to touch a\nfile another active build unit owns for anything beyond the one hook\ncall already made in `intake_service.py` itself.","operationId":"start_equipment_issue_conversation_route_api_equipment_issue_conversation_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartEquipmentConversationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Start Equipment Issue Conversation Route Api Equipment Issue Conversation Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/suppliers":{"get":{"tags":["equipment"],"summary":"List Suppliers Route","operationId":"list_suppliers_route_api_equipment_parts_suppliers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Suppliers Route Api Equipment Parts Suppliers Get"}}}}}},"post":{"tags":["equipment"],"summary":"Add Supplier Route","operationId":"add_supplier_route_api_equipment_parts_suppliers_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSupplierRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Add Supplier Route Api Equipment Parts Suppliers Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/quotes":{"post":{"tags":["equipment"],"summary":"Add Quote Route","description":"A person typing in a price they found -- the SAME `part_source_\nquotes` row `find_part_sources` writes, `source='person'` instead of\n`'rooster-search'`.","operationId":"add_quote_route_api_equipment_parts_quotes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddQuoteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Add Quote Route Api Equipment Parts Quotes Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/{part_number}/sources":{"get":{"tags":["equipment"],"summary":"Part Sources Route","description":"The \"Where to buy\" panel's own read -- ranked (price, condition,\nstock, OEM vs aftermarket; a compatibility mismatch always last).","operationId":"part_sources_route_api_equipment_parts__part_number__sources_get","parameters":[{"name":"part_number","in":"path","required":true,"schema":{"type":"string","title":"Part Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Part Sources Route Api Equipment Parts  Part Number  Sources Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/{part_number}/sources/find":{"post":{"tags":["equipment"],"summary":"Find Sources Route","description":"The \"Find sources\" button -- Rooster's own web search, through the\nSAME browser port `app/modules/finance/ubeo_portal.py` already drives.","operationId":"find_sources_route_api_equipment_parts__part_number__sources_find_post","parameters":[{"name":"part_number","in":"path","required":true,"schema":{"type":"string","title":"Part Number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindSourcesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Find Sources Route Api Equipment Parts  Part Number  Sources Find Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/{equipment_id}/parts/orders":{"post":{"tags":["equipment"],"summary":"Propose Part Order Route","operationId":"propose_part_order_route_api_equipment__equipment_id__parts_orders_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposePartOrderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Propose Part Order Route Api Equipment  Equipment Id  Parts Orders Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["equipment"],"summary":"List Part Orders Route","operationId":"list_part_orders_route_api_equipment__equipment_id__parts_orders_get","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Part Orders Route Api Equipment  Equipment Id  Parts Orders Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/orders/{order_id}/approve":{"post":{"tags":["equipment"],"summary":"Approve Part Order Route","operationId":"approve_part_order_route_api_equipment_parts_orders__order_id__approve_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Part Order Route Api Equipment Parts Orders  Order Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/orders/{order_id}/reject":{"post":{"tags":["equipment"],"summary":"Reject Part Order Route","operationId":"reject_part_order_route_api_equipment_parts_orders__order_id__reject_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reject Part Order Route Api Equipment Parts Orders  Order Id  Reject Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/orders/{order_id}/ordered":{"post":{"tags":["equipment"],"summary":"Record Ordered Route","operationId":"record_ordered_route_api_equipment_parts_orders__order_id__ordered_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordOrderedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Ordered Route Api Equipment Parts Orders  Order Id  Ordered Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/orders/{order_id}/arrived":{"post":{"tags":["equipment"],"summary":"Record Arrived Route","operationId":"record_arrived_route_api_equipment_parts_orders__order_id__arrived_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordArrivedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Arrived Route Api Equipment Parts Orders  Order Id  Arrived Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/orders/{order_id}/installed":{"post":{"tags":["equipment"],"summary":"Record Installed Route","operationId":"record_installed_route_api_equipment_parts_orders__order_id__installed_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordInstalledRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Installed Route Api Equipment Parts Orders  Order Id  Installed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/orders/ingest-confirmation-email":{"post":{"tags":["equipment"],"summary":"Ingest Confirmation Email Route","description":"A supplier's order-confirmation email, forwarded and pasted in --\nparses order number/items/price/shipping/tracking and matches it to\nthe open `part_orders` row it belongs to. See docs/runbooks/\nRUNBOOK_PART_SOURCING.md for the forwarding step.\n\n⛔UPDATED 2026-09-30 (equipment.parts_spend): the Gmail poller now calls\nthis automatically for every message it reads off tara@'s mailbox\n(`customer_service.inbound._process_email` ->\n`parts_spend.handle_inbound_mailbox_email`) -- this route stays for a\nperson forcing a re-read of one email by hand (or a supplier this\nbuild's `should_attempt_parts_parse` heuristic missed), and unlike the\nautomatic path it does NOT itself write `supplier_order_confirmations`\nor post to `operating_costs` -- it only updates the matched\n`part_orders` row(s), same as it always has.","operationId":"ingest_confirmation_email_route_api_equipment_parts_orders_ingest_confirmation_email_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestConfirmationEmailRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ingest Confirmation Email Route Api Equipment Parts Orders Ingest Confirmation Email Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/equipment/parts/spend-summary":{"get":{"tags":["equipment"],"summary":"Parts Spend Summary Route","description":"Company-wide parts spend, per supplier and per month -- Frederick:\n\"show parts spend per equipment unit, per supplier, and per month.\"\nPer-equipment spend is on `GET /api/equipment/{id}` and `/history`\ninstead (`parts_spend_cost`/`parts_spend_breakdown`, the SAME \"on the\nequipment page for that unit\" ask), because it is scoped to one\nmachine already in view, not a company-wide listing.","operationId":"parts_spend_summary_route_api_equipment_parts_spend_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Parts Spend Summary Route Api Equipment Parts Spend Summary Get"}}}}}}},"/api/issues":{"get":{"tags":["issues"],"summary":"List Issues","operationId":"list_issues_api_issues_get","parameters":[{"name":"lane","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lane"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Severity"}},{"name":"owner","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner"}},{"name":"reporter_kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reporter Kind"}},{"name":"waiting_on_a_person","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Waiting On A Person"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"reporter_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Reporter Principal Id"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Issues Api Issues Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}":{"get":{"tags":["issues"],"summary":"Get Issue","operationId":"get_issue_api_issues__issue_id__get","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Issue Api Issues  Issue Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}/answer":{"post":{"tags":["issues"],"summary":"Answer Build Question","description":"Answer the question the builder is waiting on, and requeue the issue.\n\nFrederick, 2026-07-29: \"once they respond to it, it gets back in queue...\nand then you work the list again. When you get to it again, you pick up\nwhere it left off with additional information.\"\n\nThe answer is carried into the next attempt's prompt, so the retry resumes\nrather than restarts -- an answer nobody reads is the same as never having\nasked. It does not spend an attempt, because being asked a question was\nnever a failed one.","operationId":"answer_build_question_api_issues__issue_id__answer_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerBuildQuestionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Answer Build Question Api Issues  Issue Id  Answer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}/transition":{"post":{"tags":["issues"],"summary":"Transition Issue","operationId":"transition_issue_api_issues__issue_id__transition_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransitionIssueRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Transition Issue Api Issues  Issue Id  Transition Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}/acknowledge-deployed":{"post":{"tags":["issues"],"summary":"Acknowledge Deployed","description":"The reporter says \"got it\" once their fix is live in production.\n\nFrederick, 2026-08-08: \"once it's out in production, it should then\nfollow up and let them know, and then acknowledge that it's in\nproduction.\" The DM and the Needs You card are raised automatically the\nmoment a deploy takes the issue live (production_promotion.py); this is\nthe reporter's other half. Not a `transition_issue()` call -- `deployed`\nis terminal, there is nothing left to move to, only an acknowledgement\nto record.\n\nAlso the reporter's \"mark completed\" for the two other terminal code-lane\nstates with no build to deploy: `fixed_upstream` (Marc's own fix shipped\nin his repo) and `answered` (Rooster closed it with an explanation\ninstead of building it) -- see `service.acknowledge_deployment`'s own\ndocstring for why the same fact and the same column cover all three.","operationId":"acknowledge_deployed_api_issues__issue_id__acknowledge_deployed_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Acknowledge Deployed Api Issues  Issue Id  Acknowledge Deployed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}/owner":{"patch":{"tags":["issues"],"summary":"Assign Owner","description":"The board's small owner picker (design/Issues.dc.html).","operationId":"assign_owner_api_issues__issue_id__owner_patch","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignOwnerRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Assign Owner Api Issues  Issue Id  Owner Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/intake/converse":{"post":{"tags":["issues"],"summary":"Converse With Rooster","description":"Say something to Rooster about a bug. Persists your turn and returns.\n\nCalls no model. Rooster is a Goose agent on Frederick's own\nsubscription, dispatched by the local worker, so his reply lands on the\nconversation a moment later and the chat polls for it. This used to hold\nthe request open across an Anthropic call, which meant a slow model\nlooked like a frozen page.\n\n`attachment_ids` are files already staged by POST /intake/attach --\npassing them here is what folds a pasted screenshot and the sentence\ndescribing it into ONE turn (see `ConverseRequest.attachment_ids` and\n`intake_service.say`).","operationId":"converse_with_rooster_api_issues_intake_converse_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__issues__routes__ConverseRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Converse With Rooster Api Issues Intake Converse Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}/approve-build":{"post":{"tags":["issues"],"summary":"Approve Build Route","description":"The founder approves building a feature request.\n\nUntil this happens the feature cannot be worked on at all: no branch, no\nstaging, nothing. That is the earlier of the two gates, and the one that\nstops software changing because something did not meet a preference.","operationId":"approve_build_route_api_issues__issue_id__approve_build_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Build Route Api Issues  Issue Id  Approve Build Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/work-orders":{"post":{"tags":["issues"],"summary":"File Work Order","description":"File a build item straight into the drain's queue, on the manager's\nown service principal -- Frederick, 2026-09-30: \"I want everything on\nthe cloud code seat... bring the Mac completely out of the loop for\nRoost.\" The manager no longer dispatches a builder as a local\nsubprocess in a Mac worktree; it calls this route instead, and a cloud\nseat claims and builds the issue exactly like any other (see\n`service.file_work_order`'s own docstring for the full contract,\nincluding why `ruling_reference` is required and never optional).","operationId":"file_work_order_api_issues_work_orders_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileWorkOrderRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response File Work Order Api Issues Work Orders Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/{issue_id}/revert-autonomous-fix":{"post":{"tags":["issues"],"summary":"Revert Autonomous Fix","description":"Frederick's own described interaction, verbatim (2026-08-09): \"if I\nwant to undo it, I could even go to that issue and revert it, click a\nbutton that says revert -- the fix he just put -- or if I'm fine with\nit, I can leave it alone.\" Doing nothing is the default -- this route is\nthe ONLY way an autonomous fix (app/modules/audit/railway_autofix.py)\nis ever undone; nothing in this codebase calls it unattended. Called\nfrom the issue detail screen's own \"Revert this fix\" card\n(apps/web/src/pages/issues/IssueDetailPage.tsx), drawn only for an\nissue that actually carries an autonomous fix and only for someone\nholding the grant this route requires.\n\nGated on `issues:approve_production` -- the decorator (`REVERT_AUTOFIX`,\nmodule-level, see this module's own docstring) AND, redundantly,\n`railway_autofix.revert_fix` itself (`RevertNotPermitted`) -- the same\ngrant that gates a production deploy, because undoing a production\ninfrastructure change is exactly as consequential as approving one.","operationId":"revert_autonomous_fix_api_issues__issue_id__revert_autonomous_fix_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revert Autonomous Fix Api Issues  Issue Id  Revert Autonomous Fix Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/intake/attach":{"post":{"tags":["issues"],"summary":"Attach To Intake","description":"STAGE a screenshot or document against a bug report. Writes NO turn.\n\nReached by the paperclip, a paste, or a drop onto the composer in\ndesign/Issues.dc.html's chat -- three UI paths that all end up here with\nthe same FormData shape. A reporter who can show the error does not have\nto describe pixels in prose, and Frederick's flow has Rooster collecting\n\"screenshots whatever is needed\" before he fixes anything.\nALLOWED_ATTACHMENT_TYPES also covers plain text, CSV, JSON, PDF and\ncommon office documents, so this is not always a picture.\n\nA paste used to write a fake \"(screenshot attached)\" turn the instant the\nfile landed -- BEFORE there was anything to say about it, and before the\nreporter had typed a word. Frederick: \"the screenshot shows up in the\nthread before you even submit the form... I told you to duplicate the\nsame message box function that you have in Claude Code.\" One thought\nbecame four turns: an upload turn, Rooster answering it, a text turn,\nRooster answering that too. This route now only stores the bytes and\nhands back their ids -- POST /intake/converse (`intake_service.say`) is\nthe one place a turn is ever written, words and attachments together, the\nmoment Send is actually pressed. The composer holds these as a removable\ntray of thumbnails in the meantime (IssueIntakeChat.tsx's `attached`\nstate); nothing here is shown \"in the thread\" until that happens.\n\nAttaching with no conversation yet opens a blank one\n(`intake_service.open_conversation`), so a person can drop a file in\nfirst and type afterwards -- opening it is not the same as speaking in\nit: no turn is written either way.","operationId":"attach_to_intake_api_issues_intake_attach_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_attach_to_intake_api_issues_intake_attach_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Attach To Intake Api Issues Intake Attach Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/intake/{conversation_id}/attach/{attachment_id}":{"get":{"tags":["issues"],"summary":"Read Intake Attachment","description":"The bytes of one attachment, so the thread can SHOW it.\n\nThere was no way to read an attachment back. It could be uploaded, listed by\nname, and deleted -- but never displayed, which is why a screenshot pasted\ninto Talk to Rooster appeared as the words \"(screenshot attached)\" and\nnothing else. Frederick sent the same picture three times: \"the screenshot\nis not being submitted to the ticket thread\". It was being stored. It simply\ncould not be fetched.\n\nScoped to the conversation AND checked against its reporter, so an id\nguessed from another conversation reads nothing.","operationId":"read_intake_attachment_api_issues_intake__conversation_id__attach__attachment_id__get","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}},{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Attachment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["issues"],"summary":"Remove Intake Attachment","description":"Take a pasted screenshot back off, before the issue is filed.","operationId":"remove_intake_attachment_api_issues_intake__conversation_id__attach__attachment_id__delete","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}},{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Attachment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Intake Attachment Api Issues Intake  Conversation Id  Attach  Attachment Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/intake/{conversation_id}":{"get":{"tags":["issues"],"summary":"Read Intake Conversation","description":"The conversation as it stands, which is how the chat learns Rooster\nhas answered.","operationId":"read_intake_conversation_api_issues_intake__conversation_id__get","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Intake Conversation Api Issues Intake  Conversation Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/intake/{conversation_id}/abandon":{"post":{"tags":["issues"],"summary":"Abandon Intake Conversation","description":"Drop a report the reporter started and does not want to finish.\n\nFrederick, 2026-08-12: \"there has to be a way to cancel a ticket... should\nthere be a button for that?\" There should. Before this there was no way at\nall -- a half-written report stayed open forever and the board's header\nbutton kept saying \"Resume\".\n\n409 on a FILED conversation: it produced a real issue, and that issue has\nits own disposition path. Nothing is deleted -- see `intake_service.abandon`\nfor why the transcript is kept.","operationId":"abandon_intake_conversation_api_issues_intake__conversation_id__abandon_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Abandon Intake Conversation Api Issues Intake  Conversation Id  Abandon Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/issues/intake/{conversation_id}/confirm":{"post":{"tags":["issues"],"summary":"Confirm Intake Issue","description":"Files the real issue from the conversation's current draft -- the\none place a \"Talk to Rooster\" conversation ever produces a real Issue,\nalways an explicit human action, never automatic.","operationId":"confirm_intake_issue_api_issues_intake__conversation_id__confirm_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Confirm Intake Issue Api Issues Intake  Conversation Id  Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/gitlab/webhook":{"post":{"tags":["gitlab"],"summary":"Gitlab Webhook","description":"`POST /api/gitlab/webhook` -- the group webhook's one delivery target\nfor `pigeonly`, every project, every event `create_group_hook` (E2)\nenabled. Verifies `X-Gitlab-Token` before looking at the body at all.","operationId":"gitlab_webhook_api_gitlab_webhook_post","parameters":[{"name":"X-Gitlab-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Gitlab-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Gitlab Webhook Api Gitlab Webhook Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets":{"get":{"tags":["customer_service"],"summary":"List Tickets","operationId":"list_tickets_api_cs_tickets_get","parameters":[{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kind"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"reply_channel","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply Channel"}},{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"requester_email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester Email"}},{"name":"assignee_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assignee Principal Id"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"arrived_in_spam_folder","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Arrived In Spam Folder"}},{"name":"worked_by","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Worked By"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"dir","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dir"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Tickets Api Cs Tickets Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["customer_service"],"summary":"Create Ticket","operationId":"create_ticket_api_cs_tickets_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Ticket Api Cs Tickets Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/counts":{"get":{"tags":["customer_service"],"summary":"Ticket Counts","description":"Design's Government / Consumer department tabs with live counts.","operationId":"ticket_counts_api_cs_tickets_counts_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"reply_channel","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply Channel"}},{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"requester_email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester Email"}},{"name":"assignee_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assignee Principal Id"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"arrived_in_spam_folder","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Arrived In Spam Folder"}},{"name":"worked_by","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Worked By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ticket Counts Api Cs Tickets Counts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/owners":{"get":{"tags":["customer_service"],"summary":"Ticket Owners","description":"The queue's Owner filter options -- registered BEFORE\n/tickets/{ticket_id} for the same routing reason /tickets/counts is\n(a literal \"owners\" segment would otherwise 422 trying to parse as a\nUUID rather than ever reaching this handler). Real principals who\nactually own a ticket the caller can see, never the admin roster --\nsee supervisor_service.list_ticket_owners.","operationId":"ticket_owners_api_cs_tickets_owners_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ticket Owners Api Cs Tickets Owners Get"}}}}}}},"/api/cs/tickets/organizations":{"get":{"tags":["customer_service"],"summary":"Ticket Organizations","description":"The queue's Organization filter options (Frederick: \"I don't see the\norganization filters -- where are they?\"). Real organizations that\nactually own a visible ticket, never the full `organizations` table --\nsee supervisor_service.list_ticket_organizations's own docstring for why\nthat scoping is correct for a FILTER (unlike the assignee picker's own\ndeliberately-unscoped list just below).\n\nRegistered BEFORE /tickets/{ticket_id} for the same routing reason\n/tickets/owners, /tickets/counts and /tickets/assignable-colleagues are.\n\n`q`: server-side search over organizations.name (typeahead) -- with\n1,238 organizations on file, this is how the picker reaches one\nalphabetically late without ever paging a full list client-side (the\nexact defect this same build fixed for the Customer column's own name;\nsee Ticket.org_name). `org_id`: resolve one specific organization's name\n(what the picker uses to redisplay a value already in the URL) --\nmutually exclusive with `q` in supervisor_service, `org_id` wins.","operationId":"ticket_organizations_api_cs_tickets_organizations_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ticket Organizations Api Cs Tickets Organizations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/assignable-colleagues":{"get":{"tags":["customer_service"],"summary":"Assignable Colleagues","description":"The ticket detail assignee picker's roster -- deliberately a\nDIFFERENT list than GET /tickets/owners above (the Owner FILTER), and a\ndifferent query than that route's handler runs. See\nsupervisor_service.list_assignable_colleagues and its module's \"Owner\nfilter\" section header for why: that route only ever lists people who\nalready own a visible ticket, which is a deadlock for the first\nassignment to any colleague (the exact bug Frederick hit live on\nstaging, 2026-07-30) -- this route lists every active human, whether or\nnot they own anything yet.\n\nRegistered BEFORE /tickets/{ticket_id} for the same routing reason\n/tickets/owners and /tickets/counts are above.\n\nGated the SAME way as /tickets/owners and /tickets/counts -- at least\none of tickets:b2c/tickets:b2g, via `_allowed_kinds` -- deliberately NOT\nadmin:people:read (GET /api/platform/principals's gate). A CS agent\nreassigning a ticket to a teammate has no reason to hold the admin\nPeople-tab grant, and requiring it would 403 the exact person this\npicker is for. Unlike /tickets/owners, the returned roster itself is\nNOT further scoped to `kinds` -- see\nsupervisor_service.list_assignable_colleagues's docstring for why a\nbroad, all-active-humans list is the correct one here. The caller still\nneeds a real CS grant to reach this route at all; PATCH\n/tickets/{id}/assignee still requires the per-ticket-kind grant to\nperform the assignment (_require_kind_grant, unchanged) -- this route\nonly widens who may APPEAR as a target, never who may act.","operationId":"assignable_colleagues_api_cs_tickets_assignable_colleagues_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Assignable Colleagues Api Cs Tickets Assignable Colleagues Get"}}}}}}},"/api/cs/tickets/spam":{"get":{"tags":["customer_service"],"summary":"Spam Tickets","description":"The hidden spam queue's own deliberate surface (Frederick, 2026-07-30:\n\"hidden ... but still reachable somewhere deliberate, so a false\npositive can be found and recovered\"). Every OTHER ticket-listing route\nin this module excludes is_spam unconditionally; this is the one place\nthat shows it, scoped to the caller's own kind grants like every other\nlist here.\n\nRegistered BEFORE /tickets/{ticket_id} for the same routing reason\n/tickets/owners, /tickets/counts and /tickets/assignable-colleagues are\nabove -- \"spam\" would otherwise parse as a ticket_id.","operationId":"spam_tickets_api_cs_tickets_spam_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Spam Tickets Api Cs Tickets Spam Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/clean-slate":{"get":{"tags":["customer_service"],"summary":"Clean Slate Queue","description":"Every unfinished ticket (status NOT IN resolved/closed) the caller's\ngrants allow, tagged with which of supervisor_service.CLEAN_SLATE_\nBUCKETS it belongs in -- see gather_clean_slate_queue's own module\ncomment for why this is a hard \"present everything, hide nothing\" read,\nnever a filtered or ranked one.\n\nRegistered BEFORE /tickets/{ticket_id} for the same routing reason\n/tickets/owners, /tickets/counts, /tickets/assignable-colleagues and\n/tickets/spam are above -- \"clean-slate\" would otherwise parse as a\nticket_id.","operationId":"clean_slate_queue_api_cs_tickets_clean_slate_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Clean Slate Queue Api Cs Tickets Clean Slate Get"}}}}}}},"/api/cs/tickets/bulk-transition":{"post":{"tags":["customer_service"],"summary":"Bulk Transition Tickets","description":"The clean-slate desk's bulk action -- one status change, applied to\nevery id in `ticket_ids`, via service.bulk_transition_tickets (itself a\nper-ticket loop over the SAME transition_ticket every single-ticket\nPOST .../transition call already uses -- no second write path).\n\nPer-ticket-kind grant, checked BEFORE any write, same as every other\nhandler in this module -- but resolved in ONE query\n(service.ticket_kinds) rather than one round trip per ticket in a\n300-row sweep: an id this principal cannot see is reported as its own\nper-ticket failure ('not_found' / 'forbidden'), never silently dropped\nand never allowed to abort the ids that DO pass. `skip_auto_assign=True`\non every one of them is the whole point of going through\nbulk_transition_tickets instead of a loop calling the single-ticket\nroute's own service.transition_ticket directly -- see that function's\nown docstring: a sweep must never make 300 tickets belong to whoever\nclicked the button (Frederick, 2026-08-09, live: \"I hit exactly this\ntonight and put 103 tickets on Frederick's name\").","operationId":"bulk_transition_tickets_api_cs_tickets_bulk_transition_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTransitionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Bulk Transition Tickets Api Cs Tickets Bulk Transition Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/email-ticket":{"post":{"tags":["customer_service"],"summary":"Open Email Ticket For This Customer","description":"Email the customer whose PHONE ticket this is: start a NEW EMAIL\nTICKET for them, put the typed email on it, and send it from there.\n\nFrederick, 2026-10-07, issue #2002: \"Email always to email. And then you\ncan only make an outbound physical voice call to a phone ticket. Let's\njust keep it simple. Separated. And that keeps it clean.\" and \"We should\nnot allow anyone to send in email from a voice callback. They should have\nto that should just create basically a new email ticket.\" The lead's\nreflection, confirmed with \"yes\": \"On a phone ticket, emailing the\ncustomer starts a new email ticket; the email and any reply live there,\nnever on the phone ticket.\" The phone ticket is not touched.\n\nREPLACES the 2026-09-22 route of the same path (ISS-831), which created an\nEMPTY email ticket and sent the person to its composer. The reason it\nexists survives unchanged and is why authority comes from the SOURCE\nticket: #1873 removed the composer from phone tickets, and a person can\nhold a phone ticket with NEITHER pool grant, by assignment (Frederick,\n2026-08-09: \"anybody in the company can be a customer service agent ...\nif that ticket is assigned to them\"). `_require_kind_grant_or_assignee`\nbelow is that rule; widening `POST /tickets` instead would let somebody\nholding ONE ticket open correspondence with anybody in that kind. The new\nticket is assigned to the caller, or an assignee-only agent would be sent\nto a ticket they cannot read.\n\n200 carries `sent`. False means the new ticket and the typed words exist\nas a draft and `problem` says why -- the person is taken there to fix it,\nand nothing is reported as sent that was not.\n\n⛔`delivery_unconfirmed` (third review of PR #4213, SHOULD 2): True means\nthe provider's answer is unknown and the email MAY have gone. `sent` is\nFalse and `problem` says delivery could not be confirmed and to check the\nnew email ticket before sending again; the screen must not say \"not\nsent\". `replayed` is True when this answer is the one an earlier press of\nthe same `idempotency_key` produced (SHOULD 1): nothing new was made.","operationId":"open_email_ticket_for_this_customer_api_cs_tickets__ticket_id__email_ticket_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartEmailTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Email Ticket For This Customer Api Cs Tickets  Ticket Id  Email Ticket Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/phone-ticket/numbers":{"get":{"tags":["customer_service"],"summary":"Phone Ticket Numbers For This Customer","description":"The numbers on the contact record of the customer whose EMAIL ticket\nthis is, to pick one when starting a phone ticket. Creates nothing.\n\nFrederick, 2026-10-07, issue #2002: \"Email always to email. And then you\ncan only make an outbound physical voice call to a phone ticket.\" and, on\nwhether a number on the contact record is enough to call from a new phone\nticket: \"Yes. The number on a customer's contact record should be\nenough.\"\n\n200 always carries `numbers` (possibly empty) and `problem` (the server's\nown sentence, or null): no number on the record, a customer that is not\nexactly one contact, an open phone ticket already started from this one\n(`open_ticket_id`). 409 only for a ticket that is already a phone ticket.\nAuthority comes from the SOURCE ticket, as the email twin does.\n\nFor a CONSUMER (b2c) ticket that names no contact, the customer's contact\nrecord is their Pigeonly account and `api` reads it (reads only; PR #4221,\nOPEN_ITEMS CALLOUT-18). The adapter is `act_api`, built on this request's\nown connection like the start route's, so one request holds one\nconnection; this is a click on a button, not a list that refreshes.","operationId":"phone_ticket_numbers_for_this_customer_api_cs_tickets__ticket_id__phone_ticket_numbers_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Phone Ticket Numbers For This Customer Api Cs Tickets  Ticket Id  Phone Ticket Numbers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/phone-ticket":{"post":{"tags":["customer_service"],"summary":"Open Phone Ticket For This Customer","description":"Call the customer whose EMAIL ticket this is: start a NEW PHONE TICKET\nfor them with the number the person picked from the customer's contact\nrecord, from which the call is placed.\n\nFrederick, 2026-10-07, issue #2002: \"Email always to email. And then you\ncan only make an outbound physical voice call to a phone ticket.\" The\nlead's reflection, confirmed with \"yes\": \"On an email ticket, calling the\ncustomer starts a new phone ticket; the call lives there, never on the\nemail ticket.\" And, asked \"should a number on the customer's contact\nrecord be enough for an agent to call them from a new phone ticket?\":\n\"Yes. The number on a customer's contact record should be enough.\"\n\nRESTORED 2026-10-07 after the route of the same path was unregistered in\nthe review of #4213 (the ticket it started could be neither dialled from\nnor closed). It can be now: the picked number is recorded as CS-21b\nevidence (`customer_dial.RULE_CONTACT_RECORD`) and a ticket nobody dials\nfrom can be ended (`POST /tickets/{id}/end-without-a-call`).\n\nAuthority comes from the SOURCE ticket (`_require_kind_grant_or_assignee`),\nand the new ticket is assigned to the caller. This starts the ticket and\ndoes not dial: the call is placed from the new ticket by the ordinary\npaths, which keep their own rules (`customer_dial`, the calling hours, the\ncertification gate). 409 with the server's sentence when nothing was\ncreated: more than one or no contact, no number on the record, a number\nthat is not on it, a phone ticket already open from this one.","operationId":"open_phone_ticket_for_this_customer_api_cs_tickets__ticket_id__phone_ticket_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartPhoneTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Phone Ticket For This Customer Api Cs Tickets  Ticket Id  Phone Ticket Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/end-without-a-call":{"post":{"tags":["customer_service"],"summary":"End This Phone Ticket Without A Call","description":"End a phone ticket that was started from an email ticket and never\ndialled from. Solved, which closes it (CS-59), by the statuses every ticket has; no new\nstatus. Frederick, 2026-09-21: \"You should not be able to have a second\ndisposition.\" stays true: only a ticket with NO call on it can be ended\nhere, and a ticket with a call is ended by that call's disposition.\n409 with the server's sentence for any other ticket.","operationId":"end_this_phone_ticket_without_a_call_api_cs_tickets__ticket_id__end_without_a_call_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response End This Phone Ticket Without A Call Api Cs Tickets  Ticket Id  End Without A Call Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}":{"get":{"tags":["customer_service"],"summary":"Get Ticket","operationId":"get_ticket_api_cs_tickets__ticket_id__get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Ticket Api Cs Tickets  Ticket Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/agent-metrics":{"get":{"tags":["customer_service"],"summary":"Agent Metrics","description":"Deflection % + escalation-reason breakdown (module doc: \"deflection\nstat (target 90%), escalation reasons breakdown\"). See\nsupervisor_service's module docstring for exactly what \"escalation\nreason\" means here and why the breakdown is genuinely empty until the\nConstitution allows at least one agent act.","operationId":"agent_metrics_api_cs_agent_metrics_get","parameters":[{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Window Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Metrics Api Cs Agent Metrics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/spot-check-feed":{"get":{"tags":["customer_service"],"summary":"Spot Check Feed","description":"Module doc: \"a spot-check feed -- a sample of AI-resolved tickets the\nsupervisor grades Good / Needs work.\" Real audit rows\n(cs_agent_act_log); genuinely empty until the Constitution allows at\nleast one agent act — see supervisor_service.spot_check_feed.","operationId":"spot_check_feed_api_cs_spot_check_feed_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Spot Check Feed Api Cs Spot Check Feed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/agent-acts":{"get":{"tags":["customer_service"],"summary":"Ticket Agent Acts","description":"Ticket detail's \"Agent's work\" context group (module doc: right rail\ngroup \"what it tried, why it handed off\") — this ticket's own slice of\ncs_agent_act_log. Gated the same per-ticket-kind way as get_ticket.","operationId":"ticket_agent_acts_api_cs_tickets__ticket_id__agent_acts_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Ticket Agent Acts Api Cs Tickets  Ticket Id  Agent Acts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/needs-you":{"get":{"tags":["customer_service"],"summary":"Needs You","description":"THE desk landing (docs/SUPERVISION_DESK.md; Frederick, 2026-08-09,\ndefining it precisely): \"What is on your list to work? Whether it be\nunassigned tickets for consumer, or assigned tickets for government\naccount managers, or tickets that have been assigned to you that you\nhave to respond to.\" One row per ticket that actually needs a reply\nfrom THIS caller -- see supervisor_service.gather_needs_you_queue's own\nmodule comment for the full \"mine\" composition and the\napprove/decide/rescue categorization.\n\n`pool_kinds` IS QUEUE MEMBERSHIP, NOT VERTICAL VISIBILITY (2026-08-09,\nsame-day correction, Frederick viewing this exact page as Keith\nBowdle, Sales Manager, on staging: a government ticket nobody had\nassigned to him was sitting here with an Approve action -- \"This is\nnot a ticket that needs to be assigned to him ... the pool sources\nshould key on being a CS agent / account manager for that vertical,\nnot on holding its view grant\"). This used to be `_allowed_kinds`\n(holds tickets:b2c/tickets:b2g at all) -- wrong, because that grant\nalso means \"may view/search,\" which Sales Manager holds for a\ncompletely different reason (routing a sales inquiry that arrives on\nthe government side). `registry.queue_membership_kinds` is the\nnarrower, role-derived signal (Account Manager / Customer Service by\nname, registry.QUEUE_MEMBERSHIP_ROLES) -- see that constant's own\ndocstring for the full three-tier argument. The owner (admin:people:read)\nis a deliberate ADDITION to that signal, never a full replacement of it\n(2026-08-11 fix, Cindy Palma: Frederick set her grants to tickets:b2g\nonly -- \"she still sees consumer tickets\" -- because this used to swap\nqueue membership out entirely for the FULL kind list the instant\nadmin:people:read was held, regardless of which verticals the holder\nactually had). `registry.owner_bypass_ticket_kinds` never returns a kind\nthe caller does not separately hold tickets:b2c/tickets:b2g for -- see\nits own docstring. Frederick's desk stays exactly as full as before (he\nholds both grants), Cindy's stops including b2c.\n\nNO GRANT GATE (2026-08-09, folding MyAssignedTicketsPanel's intent in\nhere rather than keeping a second, parallel \"tickets for you\" list):\nholding neither tickets:b2c nor tickets:b2g used to 403 this whole\nendpoint. That is wrong for the exact case this desk exists to cover --\na media/legal/sales inquiry Frederick assigns straight to a named\nperson who does no other CS work (his own example, the Keith Bowdle\nshape). The ASSIGNED-TO-ME half of the query applies regardless of any\ngrant or role -- an explicit, by-name assignment is never affected by\nqueue membership. A caller with no queue membership and no ticket\nassigned to them gets an honestly empty list, never a 403 -- \"nothing\nneeds you\" is correct, not broken, for most of the company most of the\ntime.\n\n`counts`/`total` are computed from the SAME `items` this response\nreturns, never a second query -- the literal fix for the desk reading\n\"Needs you 23\" while rendering one row: the number on the tab can never\ndisagree with what is on the screen because they are the same list.","operationId":"needs_you_api_cs_needs_you_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Needs You Api Cs Needs You Get"}}}}}}},"/api/cs/department-overview":{"get":{"tags":["customer_service"],"summary":"Department Overview","description":"Customer Service Analytics -- the manager/admin view (docs/\nSUPERVISION_DESK.md, Frederick, 2026-08-06: \"how is the department\nperforming, how many tickets are waiting on Cindy, how many on Milly\").\nGated on admin:people:read -- that grant decides who may OPEN this page,\nnever which ticket kinds it may show them (2026-08-11 fix, Cindy Palma:\nthis used to hardcode BOTH kinds unconditionally the instant the gate\npassed, which handed a b2g-only Account Manager holding the page grant\na b2c-inclusive department read -- the exact widening the gate was never\nmeant to grant). `kinds` is now the caller's own actual tickets:b2c/\ntickets:b2g grants (`_allowed_kinds`) -- both, for every admin/manager\nwho genuinely holds both (unchanged observable result for them), never\nmore than that for anyone who does not.\n\nFrederick, 2026-08-09, splitting this from the CS desk into its own\npage: \"Analytics can be a separate page ... that's only visible for the\nmanagers or the supervisor or the admins. Everybody else basically just\nhas a customer service tab.\" Rooster's performance (`rooster_by_kind`)\nis NOT part of this response -- it was briefly reintroduced here\n(2026-08-09, same-day instruction: \"on analytics, Rooster should be\ntalking about what he's done for the entire customer service\ndepartment\"), then superseded by GET /department-analytics below\n(#328, landed the same day): that endpoint's own\n`gather_rooster_department` is the real, shipped integration point for\nthe department-wide read (DepartmentAnalyticsPanel.tsx), reusing the\nSAME `gather_rooster_performance` this response would otherwise have\ncalled a second time. Two endpoints computing the identical\ndepartment-wide Rooster figures would be the one-fact-two-\nimplementations defect this codebase has a standing rule against, so\nthis endpoint stays exactly what it was: person_loads/unassigned_count/\noldest_unanswered only. GET /rooster-performance below is the THIRD,\ngenuinely different question (one person's own counts, not the\ndepartment's).","operationId":"department_overview_api_cs_department_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Department Overview Api Cs Department Overview Get"}}}}}}},"/api/cs/department-analytics":{"get":{"tags":["customer_service"],"summary":"Department Analytics","description":"Customer Service Analytics -- the department health-and-productivity\nread-model (Frederick Hutson, 2026-08-09): \"Analytics should just be all\nthe things that someone will want to know to gauge the health and\nproductivity of a customer service department ... rooted in the company\nOS's overall.\" Gated the SAME way GET /department-overview above already\nis (admin:people:read) -- this is the same manager/admin audience, a\nsecond section of the same page, not a new audience, and `kinds` is\nscoped the SAME way GET /department-overview's own fix scopes it\n(2026-08-11, Cindy Palma) -- the caller's own tickets:b2c/tickets:b2g\ngrants, never the full kind list just because the page-access gate\npassed.\n\nSee department_analytics_service's own module docstring for the full\nreuse map (lifecycle.compute_ours_seconds for every duration,\nsupervisor_service.compute_agent_metrics for deflection/escalation,\nsupervisor_service.gather_rooster_performance for the Wilson lower\nbound) and for why Rooster's figures here are DEPARTMENT-wide, a\ndifferent question than GET /rooster-performance's own per-caller desk\nview above -- see gather_rooster_department's docstring.","operationId":"department_analytics_api_cs_department_analytics_get","parameters":[{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"default":30,"title":"Window Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Department Analytics Api Cs Department Analytics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/rooster-performance":{"get":{"tags":["customer_service"],"summary":"Rooster Performance","description":"\"What Rooster has done for ME\" -- the desk's own panel (Frederick,\n2026-08-09, drawing the line against the department-wide read on\nAnalytics): \"On the desk, Rooster should be talking about what he's\ndone for you. On analytics, Rooster should be talking about what he's\ndone for the entire customer service department. Two different data\npoints.\" See `supervisor_service.PersonRoosterStat`'s own docstring for\nexactly what is counted and, as importantly, what was deliberately left\nout as not honestly computable.\n\nNO GRANT GATE, same reasoning as GET /needs-you (2026-08-09, folding\nthat endpoint's own fix in here too): tickets assigned to this caller\nby id count regardless of any grant or role (the Keith Bowdle shape).\nA caller with no queue membership and nothing Rooster has touched for\nthem gets honest zeros, never a 403.\n\n`pool_kinds` IS QUEUE MEMBERSHIP, NOT VERTICAL VISIBILITY -- same\nsame-day correction as GET /needs-you above, and the same underlying\nconflation: `_allowed_kinds` (holds tickets:b2c/tickets:b2g) used to\nscope the shared-pool half of this count too, which would have counted\nevery b2c ticket ever as \"your pool\" for anyone merely holding\ntickets:b2c visibility -- a fulfillment technician (both verticals by\ndefault since migration 0137) included, not just a Sales Manager. See\nregistry.QUEUE_MEMBERSHIP_ROLES's own docstring for the full argument;\nthe owner (admin:people:read) is the same deliberate ADDITION-not-\nreplacement GET /needs-you uses -- `registry.owner_bypass_ticket_kinds`,\nnever a kind the caller does not separately hold tickets:b2c/tickets:b2g\nfor (2026-08-11 fix, Cindy Palma: see GET /needs-you's own docstring for\nthe full incident).","operationId":"rooster_performance_api_cs_rooster_performance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Rooster Performance Api Cs Rooster Performance Get"}}}}}}},"/api/cs/directive":{"get":{"tags":["customer_service"],"summary":"Get Directive","description":"The Rooster header card's directive line (module doc: \"Rooster\nheader card + the supervisor's directive\"). Computed live on every\nrequest (unlike Today's once-a-day persisted line, module doc has no\n\"generate once daily\" rule for this one, and the underlying counts are\nminute-to-minute) — see directive_adapter.py for the live/stub\ncomposer.\n\nIf the model is unavailable (credit exhaustion, timeout, network\nerror), degrades to the deterministic StubDirectiveAdapter composed\nover the SAME gathered context rather than 502ing the whole CS\nsupervisor room -- nothing here is persisted either way, so there is no\ncaching hazard to guard against (unlike Today's direction line).","operationId":"get_directive_api_cs_directive_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Directive Api Cs Directive Get"}}}}}}},"/api/cs/tickets/{ticket_id}/research":{"post":{"tags":["customer_service"],"summary":"Open Research Desk","description":"Open the research overlay for this ticket, gathering the facts.\n\n⛔THE ANSWER WAS FOUR QUERIES AWAY AND NOBODY HAD THE QUERIES. Ticket\n#60455: Iowa DOC, via the Ombudsman, named four parcel codes. Rooster\ncould only restate the question, and the account manager had to go and\nfind every status by hand across dashboards. Frederick, 2026-09-02:\n\"allows the human to do research without having to go search and\ndashboards and all that kind of stuff.\"\n\nSame per-ticket access as opening the ticket itself -- researching a\nticket is reading it, harder.\n\n⛔IT RE-GATHERS EVERY TIME THE BUTTON IS PRESSED. Frederick, 2026-09-04:\n\"information can change ... it may not have an update on a particular\nparcel, and it come back a few hours later and the status of that parcel\nchanges. So every time you hit research this ticket, the research should\nbe refreshed to see if anything changed.\"\n\nThis used to gather once and then only on `refresh=true`, so that a\nfigure could not move under a human's earlier reasoning. Right about the\nhazard, wrong about the remedy: the answer to a figure that moved is to\nsay it moved, which `research_changed_at` now does -- and a room whose\nfacts moved since Rooster last spoke goes back into his queue by itself.\n\n`refresh=false` is for the overlay's POLLING, which asks this same\nquestion every few seconds while it waits and must not re-run the vendor\nlookups each time.","operationId":"open_research_desk_api_cs_tickets__ticket_id__research_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"refresh","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Refresh"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Research Desk Api Cs Tickets  Ticket Id  Research Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/research/brief-again":{"post":{"tags":["customer_service"],"summary":"Brief Again","description":"Ask Rooster to write the brief again.\n\n⛔A BRIEF DOES NOT REWRITE ITSELF. Opening the room re-gathers the facts\nand leaves the words exactly as they were, so a room briefed before a\nchange to how briefs are written keeps the old one for ever -- Frederick,\n2026-09-05, on a four-paragraph brief hours after the shape was cut to\ntwelve bullets: \"What is happening here? Why hasn't this updated?\"\n\nNothing was broken. Nothing had asked him to write another one.","operationId":"brief_again_api_cs_tickets__ticket_id__research_brief_again_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Brief Again Api Cs Tickets  Ticket Id  Research Brief Again Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/research/say":{"post":{"tags":["customer_service"],"summary":"Say In Research Desk","description":"The human's turn in the overlay.\n\n⛔THIS CANNOT REACH THE CUSTOMER, and that is the feature. Everything said\nin this room is working-out-loud between an account manager and Rooster.\nThe only thing that ever crosses into the ticket is a draft the human asks\nfor, and it lands in the ordinary reply box for them to edit and send.","operationId":"say_in_research_desk_api_cs_tickets__ticket_id__research_say_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchSayRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Say In Research Desk Api Cs Tickets  Ticket Id  Research Say Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/messages":{"post":{"tags":["customer_service"],"summary":"Create Message","operationId":"create_message_api_cs_tickets__ticket_id__messages_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMessageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Message Api Cs Tickets  Ticket Id  Messages Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/messages/{message_id}/attachments":{"post":{"tags":["customer_service"],"summary":"Upload Message Attachments","description":"Attach files to a not-yet-sent reply. Both verticals: gated exactly\nlike every other write on this ticket (`_require_kind_grant_or_assignee`\n-- a b2c CS agent and a b2g account manager both reach this the same\nway, through the SAME grant check every other message action here\nuses).\n\nRefuses attaching to anything but a draft, outbound message: a sent\nmessage has already left, and attaching to it now would store a file\nthe customer never received while the thread implies otherwise -- the\nsame \"never claim a file arrived when it did not\" floor\nAttachmentRejected/AttachmentTooLarge enforce per-file, applied at the\nmessage level instead.\n\nEVERY FILE VALIDATED BEFORE ANY WRITE (mirrors issues/routes.py's\nattach_to_intake): a batch that fails on file 2 of 3 stores nothing,\nrather than a half-attached reply.","operationId":"upload_message_attachments_api_cs_tickets__ticket_id__messages__message_id__attachments_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Message Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_message_attachments_api_cs_tickets__ticket_id__messages__message_id__attachments_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Upload Message Attachments Api Cs Tickets  Ticket Id  Messages  Message Id  Attachments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/transition":{"post":{"tags":["customer_service"],"summary":"Transition Ticket","operationId":"transition_ticket_api_cs_tickets__ticket_id__transition_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransitionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Transition Ticket Api Cs Tickets  Ticket Id  Transition Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/satisfaction":{"post":{"tags":["customer_service"],"summary":"Record Ticket Satisfaction","description":"migrations/0468: the one door a satisfaction rating comes in through.\n\n⛔NOT A SURVEY ENDPOINT, and deliberately not reachable by an anonymous\ncustomer following a link: gated exactly like every other write on a\nticket (_require_kind_grant_or_assignee -- the caller must hold this\nticket's kind or already own it). WHO asks the customer, and how their\nanswer gets from them to here, is a product decision nobody has made;\nbuilding an unauthenticated rating link would be making it. Until it is\nmade, the honest writer is a person who was told -- which is exactly why\n`rated_by` distinguishes 'customer' from 'manager' and the two are never\naveraged (0276's rule, kept here rather than restated differently).","operationId":"record_ticket_satisfaction_api_cs_tickets__ticket_id__satisfaction_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketSatisfactionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Ticket Satisfaction Api Cs Tickets  Ticket Id  Satisfaction Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/assignee":{"patch":{"tags":["customer_service"],"summary":"Assign Ticket Owner","description":"Pick a different owner from the desk (Frederick, 2026-07-30). Unlike\nevery auto-assign path in service.py, this ALWAYS overwrites the current\nassignee -- an explicit human choice, not an accountability side effect\nof some other action -- and 422s (AssigneeMustBeHuman) rather than\nsilently no-opping if the target is not a real, active, human\nprincipal.","operationId":"assign_ticket_owner_api_cs_tickets__ticket_id__assignee_patch","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Assign Ticket Owner Api Cs Tickets  Ticket Id  Assignee Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/requester":{"patch":{"tags":["customer_service"],"summary":"Update Ticket Requester","description":"Edit the name/email/phone this ticket has on file for the person who\nwrote in (ISS-115). Gated exactly like every other write on a ticket\n(`_require_kind_grant_or_assignee`) -- correcting a customer's address\nis desk work, not an admin action, so it needs no grant beyond the one\nthat already lets this principal work this ticket.\n\nSee service.update_ticket_requester for what it writes (the ticket's\nown three columns, never Marc's app) and what it refuses.","operationId":"update_ticket_requester_api_cs_tickets__ticket_id__requester_patch","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRequesterRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Ticket Requester Api Cs Tickets  Ticket Id  Requester Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/transfer":{"post":{"tags":["customer_service"],"summary":"Transfer Ticket Department","description":"Who may transfer: gated exactly like every other write on this\nticket (`_require_kind_grant_or_assignee`, checked against the\nticket's CURRENT kind, before anything moves) -- holding the current\nkind's grant or being its assignee. Deliberately NOT gated on also\nholding the DESTINATION kind's grant: a CS agent holding only\n`tickets:b2c` who transfers a ticket to b2g is pushing work into a\nqueue they cannot see, and that is the intended shape of this feature\n-- an attorney's mail landing on the consumer address, or (the\nproduction shape this was built from, ticket G-59979) a government\nticket whose SUBSTANCE only the consumer desk can fix, must be\nmovable by whoever is looking at it, not gated on the mover already\nhaving a foot in the other department.\n\nBecause of that, the mover can end this call holding a ticket they can\nno longer open at all -- Cindy Palma, holding only `tickets:b2g`,\ntransferring a b2g ticket she is assigned to (and has already replied\non) over to b2c, loses `_require_kind_grant_or_assignee` access to it\nthe instant this commits: her kind grant does not cover b2c, and the\nassignee bypass no longer applies because the transfer just cleared\nthe assignee. `still_accessible` says so explicitly in the response,\ncomputed the same way that gate itself would evaluate a FRESH GET on\nthis ticket right now -- so the caller can render \"moved to the\nconsumer queue; you no longer have access to it\" instead of the ticket\njust disappearing from their screen with no explanation, the exact\nsilent-disappearance pattern this build exists to avoid.","operationId":"transfer_ticket_department_api_cs_tickets__ticket_id__transfer_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Transfer Ticket Department Api Cs Tickets  Ticket Id  Transfer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/lock":{"post":{"tags":["customer_service"],"summary":"Acquire Ticket Lock","description":"`?renew=true` is the heartbeat of a tab that holds the lock: UPDATE\nonly, 409 `ticket_lock_lost` when a handoff removed it (never re-creates\nit; issue #2172 review 2026-10-07). When the caller has no row but someone\nelse holds a live lock it answers 409 `ticket_locked` naming them (issue\n#4169); `ticket_lock_lost` means nobody holds it. Without it: claim the ticket for the\ncalling principal, or renew an already-held\nclaim -- the desk calls this once on opening a ticket and again on a\nheartbeat interval while it stays open (see service.acquire_ticket_lock's\nown docstring for why renewal and first-claim are the same call). 409s\nwith a structured body (`_handle_domain_errors`'s TicketLockHeld branch)\nwhen someone else is already in it, naming who and until when -- exactly\nwhat the desk needs to render \"Rowena is in this one\" without a second\nround trip.","operationId":"acquire_ticket_lock_api_cs_tickets__ticket_id__lock_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"renew","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Renew"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Acquire Ticket Lock Api Cs Tickets  Ticket Id  Lock Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/lock/release":{"post":{"tags":["customer_service"],"summary":"Release Ticket Lock","description":"Ends the lock on this ticket right now.\n\n`?mine=true` (a page LEAVING the ticket) deletes only the caller's own\nrow, so a closing tab can never delete a colleague's or the new owner's\nlock (issue #4169, 2026-10-07). Without it the release is unqualified by\nholder -- Take over, and the \"an ordinary person on the desk can un-stick\nthis\" escape hatch docs/SUPERVISION_DESK.md requires for a lock that looks\nabandoned but has not expired yet. A no-op, not a 404, when nothing\nmatched (service.release_ticket_lock's own docstring).","operationId":"release_ticket_lock_api_cs_tickets__ticket_id__lock_release_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"mine","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Mine"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Release Ticket Lock Api Cs Tickets  Ticket Id  Lock Release Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/organization-match-preview":{"get":{"tags":["customer_service"],"summary":"Preview Organization Match","description":"Would matching this ticket to that organisation claim the domain?\n\nAsked BEFORE the match so the desk can say which of the two things is\nabout to happen -- \"everyone at @yakimacounty.gov from now on\" is a\ndifferent promise from \"just this person\", and the difference is\ninvisible afterwards.","operationId":"preview_organization_match_api_cs_tickets__ticket_id__organization_match_preview_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"org_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Preview Organization Match Api Cs Tickets  Ticket Id  Organization Match Preview Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/organization-domain-match":{"get":{"tags":["customer_service"],"summary":"Organization Domain Match For Ticket","description":"What the deterministic domain lookup makes of this ticket.\n\nFrederick, 2026-08-10: *\"If every organization already has an email\ndomain that we know what it is, that's deterministic. It should always\nbe able to identify that in any email we receive.\"*\n\n`inbound.py` already runs this on the way in, so a ticket that could be\nmatched deterministically arrives matched and never reaches this\nendpoint. What is left is exactly the two cases a human is needed for,\nand the desk should not make them work either one out for themselves:\n\n  * a ticket older than that wiring, whose answer has been sitting in\n    its own body the whole time -- offered here as a one-click match\n    through the SAME `match-organization` action a manual search ends\n    in, never applied behind their back.\n  * a domain two organisations share. There is no right answer to pick\n    (`delaware.gov` is Delaware DOC and Delaware DSCYF), so this returns\n    the ambiguity with both names in it and the person chooses.\n\nNever writes. `org_id` here is a suggestion; only POST match-organization\nchanges the ticket.","operationId":"organization_domain_match_for_ticket_api_cs_tickets__ticket_id__organization_domain_match_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Organization Domain Match For Ticket Api Cs Tickets  Ticket Id  Organization Domain Match Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/match-organization":{"post":{"tags":["customer_service"],"summary":"Match Ticket Organization","description":"\"This sender is that agency.\" Files the ticket and remembers them.\n\nFrederick, 2026-08-04: *\"the account manager, whoever's answering the\nticket, can match it to the organization... and then that domain now is\nforever linked.\"* One action, from the ticket being read, answered once.","operationId":"match_ticket_organization_api_cs_tickets__ticket_id__match_organization_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchOrganizationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Match Ticket Organization Api Cs Tickets  Ticket Id  Match Organization Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/not-interested":{"post":{"tags":["customer_service"],"summary":"Not Interested","description":"Close this ticket AND stop the sender, in one action.\n\nFrederick, 2026-07-28: \"we need a way to flag it so it closes the ticket\nbut also marks it as something we're not interested in, so if we get more\nemail like that it automatically ignores it -- we don't have to do it more\nthan once.\"\n\nBoth halves in one transaction. A close that did not suppress leaves the\nnext newsletter to be closed by hand; a suppression that did not close\nleaves the ticket sitting in the queue. Half of this action is worse than\nnone of it, so neither half lands alone.","operationId":"not_interested_api_cs_tickets__ticket_id__not_interested_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotInterestedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Not Interested Api Cs Tickets  Ticket Id  Not Interested Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/spam":{"post":{"tags":["customer_service"],"summary":"Mark Spam","description":"Block the sender, close the ticket, hide it from every normal\nsurface -- see service.mark_ticket_spam's own docstring for the shared\nsuppress-then-close machinery this reuses (the same shape\n/tickets/{id}/not-interested above already proved) and\nmigrations/0075_ticket_spam.sql for why hiding is a flag, not a status.","operationId":"mark_spam_api_cs_tickets__ticket_id__spam_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkSpamRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Spam Api Cs Tickets  Ticket Id  Spam Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/spam-suggestion/decline":{"post":{"tags":["customer_service"],"summary":"Decline Spam Suggestion","description":"A human looked at Rooster's 'suggest_spam' triage card and disagrees.\nLeaves the ticket exactly as it was -- an ordinary ticket to be worked,\nFrederick's own words (build task 2026-08-11): \"Decline leaves it as an\nordinary ticket to be worked\" -- and only clears the suggestion off the\ndesk (`action_items.dismiss`, \"this does not need doing\").\n\nGated the SAME way `mark_spam` above is -- ticket-kind grant or\nassignee (`_require_kind_grant_or_assignee`) -- deliberately NOT\n`platform:action_items:write`, the grant the generic `POST\n/api/platform/action-items/{id}/dismiss` route requires. That grant is\nbound to the Customer Service (b2c) role but not Account Manager\n(migrations/0046_ticket_access_by_role.sql), so routing this specific\nclick through the generic endpoint would leave a b2g account manager\nable to AGREE with a suggestion (mark_spam only ever checked the ticket\ngrant) but not decline one on the same ticket. Both halves of one\ngesture must open to the same person.\n\nA missing/already-resolved card is a no-op, not an error -- the same\n\"a double click, or someone else got there first, must never surface\nas a failure\" tolerance `mark_spam`'s own `resolve_action_item_id`\nbranch documents just above.","operationId":"decline_spam_suggestion_api_cs_tickets__ticket_id__spam_suggestion_decline_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decline Spam Suggestion Api Cs Tickets  Ticket Id  Spam Suggestion Decline Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/not-a-ticket":{"post":{"tags":["customer_service"],"summary":"Mark Not A Ticket","description":"Close the ticket, hide it from the CS queues and counts, and list the\nsender so their future mail never opens a CS ticket. NOT spam: nothing is\nblocked and the mail is kept -- see service.mark_ticket_not_a_ticket and\nnot_a_ticket.py. Gated like `mark_spam` (ticket-kind grant or assignee).\nUndone from System settings (DELETE /api/system/not-a-ticket-senders/{id}).","operationId":"mark_not_a_ticket_api_cs_tickets__ticket_id__not_a_ticket_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkNotATicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Not A Ticket Api Cs Tickets  Ticket Id  Not A Ticket Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/spam/recover":{"post":{"tags":["customer_service"],"summary":"Recover Spam","description":"Undo mark_spam as one coherent action: lift the suppression, un-hide\nthe ticket, reopen it. See service.recover_spam_ticket's own docstring\nfor why this is the one reopen left (an undo of a mistaken spam flag,\n`cause=CORRECTION`; the ticket was never Solved) and why it has no window.","operationId":"recover_spam_api_cs_tickets__ticket_id__spam_recover_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Recover Spam Api Cs Tickets  Ticket Id  Spam Recover Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/suppressions":{"get":{"tags":["customer_service"],"summary":"List Suppressions","description":"What we are currently ignoring, busiest rule first.\n\nVisible on purpose. A suppression list nobody can read is a mail rule\nnobody can debug -- and the one real risk here is a rule that quietly\nswallows a customer.","operationId":"list_suppressions_api_cs_suppressions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Suppressions Api Cs Suppressions Get"}}}}}}},"/api/cs/suppressions/{suppression_id}/held":{"get":{"tags":["customer_service"],"summary":"List Held Mail","description":"The mail held for this rule, oldest first: sender, subject, when it\narrived and, once released, what became of it.\n\nFrederick, 2026-10-07 (#2284): a sender a person marked as spam or\nmarketing opens no ticket, so this list is the only place the desk can see\nwhat the rule is holding back. Same audience as the rule list above. The\ndesk screen that shows it does not exist yet (OPEN_ITEMS CS-HELD-1); the\nroute is what a screen, or a person with the API, reads today.\n`state` is `held` (waiting), `released` (replayed by a revoke) or `all`.","operationId":"list_held_mail_api_cs_suppressions__suppression_id__held_get","parameters":[{"name":"suppression_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Suppression Id"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","pattern":"^(held|released|all)$","default":"held","title":"State"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Held Mail Api Cs Suppressions  Suppression Id  Held Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/suppressions/{suppression_id}":{"delete":{"tags":["customer_service"],"summary":"Revoke Suppression","description":"Stop applying a rule, and bring back the mail it was holding. The rule\nstays on the record as revoked, because the fact that we ignored this\nsender is what you need when you go looking for the mail that never\narrived.\n\nFrederick, 2026-10-07 (#2284, #2171): mail from a suppressed sender opens\nno ticket and is held whole; removing the rule re-ingests every held\nmessage through the normal inbound path, once each (`suppression_replay`).\nFrederick ruled the hold and the replay; ARRIVAL ORDER is the builder's\ndecision (recorded 2026-10-07, not Frederick's words): it is the order the\ndesk would have seen the mail. Safe to call again: a retry, or the second of two\nsimultaneous calls, revokes nothing, replays nothing twice and finishes\nanything the first attempt did not (`revoked` is false then, `released`\nsays what this call brought back). 404 only when no such rule exists.","operationId":"revoke_suppression_api_cs_suppressions__suppression_id__delete","parameters":[{"name":"suppression_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Suppression Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revoke Suppression Api Cs Suppressions  Suppression Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/unidentified-senders":{"get":{"tags":["customer_service"],"summary":"List Unidentified Senders","description":"Government mail waiting on somebody to say who wrote it.\n\nThis queue existed in the database for a fortnight with no route and no\npage, which meant the mail it was holding was invisible: refused at the\ndoor, kept safely, and seen by nobody. Four real agencies were sitting in\nit on 2026-08-03.","operationId":"list_unidentified_senders_api_cs_unidentified_senders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Unidentified Senders Api Cs Unidentified Senders Get"}}}}}}},"/api/cs/unidentified-senders/{sender_id}/identify":{"post":{"tags":["customer_service"],"summary":"Identify Unidentified Sender","description":"Say who this is. Their mail is delivered and their colleagues resolve.\n\nOne transaction: identifying a sender and delivering the mail that was\nrefused because we could not identify them are the same act, and a half\nof it would leave the queue saying answered while the email stayed in a\ndead-letter table.","operationId":"identify_unidentified_sender_api_cs_unidentified_senders__sender_id__identify_post","parameters":[{"name":"sender_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Sender Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifySenderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Identify Unidentified Sender Api Cs Unidentified Senders  Sender Id  Identify Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/unidentified-senders/{sender_id}/dismiss":{"post":{"tags":["customer_service"],"summary":"Dismiss Unidentified Sender","description":"Nobody to identify them as. Answers the question, keeps the mail.","operationId":"dismiss_unidentified_sender_api_cs_unidentified_senders__sender_id__dismiss_post","parameters":[{"name":"sender_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Sender Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Dismiss Unidentified Sender Api Cs Unidentified Senders  Sender Id  Dismiss Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/agent-not-interested":{"post":{"tags":["customer_service"],"summary":"Agent Not Interested","description":"Rooster closes mail he judges to be marketing, and gets graded on it.\n\nFrederick, 2026-07-28: \"He should. And a human looks at it like we do\neverything else, and then that adds to his 95% rating if he gets it right.\"\n\nThe ticket closes here. The sender does NOT get blocked here -- the rule\nis written 'pending' and enforces nothing until a human confirms it\n(Frederick, 2026-08-10: \"Rooster can suggest something should be spam, and\na human can agree with it\"). The `suppression` object in the response says\nwhich, via `review_state`.\n\nRefusals come back as 422 with the reason in plain words, because the\nreader is an agent deciding what to do next: 422 says \"this answer will\nnot change, stop and leave it for a human\", where a 5xx would say \"retry\".","operationId":"agent_not_interested_api_cs_tickets__ticket_id__agent_not_interested_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentNotInterestedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Not Interested Api Cs Tickets  Ticket Id  Agent Not Interested Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/suppressions/pending-review":{"get":{"tags":["customer_service"],"summary":"Suppressions Pending Review","description":"Calls Rooster made that nobody has graded yet, oldest first.","operationId":"suppressions_pending_review_api_cs_suppressions_pending_review_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Suppressions Pending Review Api Cs Suppressions Pending Review Get"}}}}}}},"/api/cs/suppressions/{suppression_id}/review":{"post":{"tags":["customer_service"],"summary":"Review Suppression","description":"Grade one of Rooster's calls. Confirming counts toward his rating;\noverturning revokes the rule and reopens the ticket he closed.","operationId":"review_suppression_api_cs_suppressions__suppression_id__review_post","parameters":[{"name":"suppression_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Suppression Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Review Suppression Api Cs Suppressions  Suppression Id  Review Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/messages/{message_id}/send":{"post":{"tags":["customer_service"],"summary":"Send Message","description":"Human action only -- an agent/automation never calls this (master\nspec §3.5: \"B2G: agent drafts, never sends\"). Gated per-ticket-kind\nexactly like every other handler here (tickets:b2c / tickets:b2g).","operationId":"send_message_api_cs_tickets__ticket_id__messages__message_id__send_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Message Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SendMessageRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Message Api Cs Tickets  Ticket Id  Messages  Message Id  Send Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/text-reply":{"post":{"tags":["customer_service"],"summary":"Send Text Reply","description":"Reply BY TEXT on a text ticket (migrations/0435): one message of at\nmost 160 characters, from the short code, to the number on the ticket.\nHuman action; gated per-ticket-kind exactly like the email send above.","operationId":"send_text_reply_api_cs_tickets__ticket_id__text_reply_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextReplyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Text Reply Api Cs Tickets  Ticket Id  Text Reply Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/messages/{message_id}/send-edited":{"post":{"tags":["customer_service"],"summary":"Send Edited Draft","description":"Send the human's OWN version of Rooster's draft.\n\n`message_id` is the AGENT's draft. The edited text is appended as a new\nhuman-authored message and sent (ticket_messages is append-only, so the\nagent's original is never rewritten), and the agent's draft is disposed\n'edited' with a unified diff -- or 'approved' if the human changed\nnothing after all. That diff is what teaches the skill; see\nservice.send_edited_draft. A person's own unsent draft (the thread row's\nEdit, ISS-570) is edited and sent the same way, with no disposition.","operationId":"send_edited_draft_api_cs_tickets__ticket_id__messages__message_id__send_edited_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendEditedDraftRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Edited Draft Api Cs Tickets  Ticket Id  Messages  Message Id  Send Edited Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/messages/{message_id}/discard":{"post":{"tags":["customer_service"],"summary":"Discard Agent Draft","description":"A human would not send any version of this draft. Records 'rejected'\n-- or, when `body.fabricated` is true, the heavier 'fabricated' outcome\n-- against the skill: the strongest negative signal Rooster can get, and\none that used to never leave the browser -- AND flips the row to\nstate='discarded', so it stops blocking Rooster from drafting again on\nthis ticket (see service.discard_agent_draft's docstring).","operationId":"discard_agent_draft_api_cs_tickets__ticket_id__messages__message_id__discard_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Message Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DiscardDraftRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Discard Agent Draft Api Cs Tickets  Ticket Id  Messages  Message Id  Discard Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/mark-rooster-abandoned":{"post":{"tags":["customer_service"],"summary":"Mark Rooster Abandoned","description":"A human on the desk confirms Rooster genuinely took a turn on this\nticket and produced nothing, rather than there being no draft yet\nbecause his turn simply has not come up -- docs/SUPERVISION_DESK.md's\n'abandoned' outcome, a plain failure and a bug to chase, told apart from\na 'proper_escalation' (a SUCCESS: Rooster knowingly refusing to invent\nan answer) precisely so the two stop looking identical on this desk.\n\nRefuses with 422 (`NoAbandonmentEvidence`) unless\n`rooster_ticket_draft_attempts` already carries a real failed turn for\nthis ticket -- see service.mark_rooster_abandoned's own docstring for\nwhy this is a confirmation of a system fact, never a human's guess.","operationId":"mark_rooster_abandoned_api_cs_tickets__ticket_id__mark_rooster_abandoned_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Rooster Abandoned Api Cs Tickets  Ticket Id  Mark Rooster Abandoned Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/agent-acts/{ticket_id}/status-lookup":{"post":{"tags":["customer_service"],"summary":"Agent Act Status Lookup","operationId":"agent_act_status_lookup_api_cs_agent_acts__ticket_id__status_lookup_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Act Status Lookup Api Cs Agent Acts  Ticket Id  Status Lookup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/agent-acts/{ticket_id}/resend-transactional":{"post":{"tags":["customer_service"],"summary":"Agent Act Resend Transactional","operationId":"agent_act_resend_transactional_api_cs_agent_acts__ticket_id__resend_transactional_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendTransactionalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Act Resend Transactional Api Cs Agent Acts  Ticket Id  Resend Transactional Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/agent-acts/{ticket_id}/file-reprint":{"post":{"tags":["customer_service"],"summary":"Agent Act File Reprint","operationId":"agent_act_file_reprint_api_cs_agent_acts__ticket_id__file_reprint_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileReprintRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Act File Reprint Api Cs Agent Acts  Ticket Id  File Reprint Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/agent-acts/{ticket_id}/address-update":{"post":{"tags":["customer_service"],"summary":"Agent Act Address Update","operationId":"agent_act_address_update_api_cs_agent_acts__ticket_id__address_update_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Agent Act Address Update Api Cs Agent Acts  Ticket Id  Address Update Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/tickets/{ticket_id}/attachments/{attachment_id}":{"get":{"tags":["customer_service"],"summary":"Get Ticket Attachment","description":"Stream one attachment to the browser.\n\nCustomers send screenshots of what went wrong, photos and PDFs, and every\none of those has to reach the person answering the ticket. The bytes live\nin Chatwoot; this is the only way they reach a browser. Company OS\nproxies them so a vendor host never appears in our markup and the file\nstays behind our own permissions rather than whatever a signed vendor\nlink happens to allow.\n\nKeyed by TICKET, not by Chatwoot conversation: the browser should not\nhave to know a vendor's id scheme to open a photo, and department scoping\nis a property of the ticket. A consumer agent cannot read a government\nattachment by guessing an id.","operationId":"get_ticket_attachment_api_cs_tickets__ticket_id__attachments__attachment_id__get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"attachment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Attachment Id"}},{"name":"download","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Download"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/chatwoot/conversation":{"post":{"tags":["customer_service"],"summary":"Chatwoot Conversation Event","description":"conversation_created, conversation_updated, conversation_status_changed.\n\nA conversation we have never seen answers `linked: false` rather than\nerroring: that is Chatwoot telling us mail arrived for something with no\nticket yet, which is normal, not a fault.","operationId":"chatwoot_conversation_event_api_cs_chatwoot_conversation_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","default":"","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatwootConversationEvent"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Chatwoot Conversation Event Api Cs Chatwoot Conversation Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/chatwoot/message":{"post":{"tags":["customer_service"],"summary":"Chatwoot Message Event","description":"message_created and message_updated.\n\nDeduped on the Chatwoot message id, so a retried webhook updates the\nmessage it already made rather than posting the customer's email into\nthe thread twice.","operationId":"chatwoot_message_event_api_cs_chatwoot_message_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","default":"","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatwootMessageEvent"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Chatwoot Message Event Api Cs Chatwoot Message Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/inbound-email":{"post":{"tags":["customer_service"],"summary":"Inbound Email","description":"SendGrid Inbound Parse target (multipart/form-data in production;\nurlencoded form or JSON also accepted -- the parsing below is content-\ntype agnostic once the fields are a str-keyed mapping). Always returns\n200 on a well-formed webhook POST, even when the email itself could not\nbe turned into a ticket (dead_lettered=true in the body) -- a non-200\nhere would make SendGrid retry-storm a request that already succeeded\nat the one thing that matters, durably recording the attempt.","operationId":"inbound_email_api_cs_inbound_email_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Inbound Email Api Cs Inbound Email Post"}}}}}}},"/api/cs/failed-ingestion/noise-count":{"get":{"tags":["customer_service"],"summary":"Failed Ingestion Noise Count","description":"How many abandoned dead letters `_dead_letter_is_expected_noise`\nsuppressed rather than filing as an issue (Frederick, 2026-08-11: noise\n\"must not\" become an issue, but it must be \"counted, and the count\nvisible somewhere\"). Same audience/gate as GET /department-overview --\nthis is an ops-health number, not a queue anyone works.","operationId":"failed_ingestion_noise_count_api_cs_failed_ingestion_noise_count_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"integer"},"type":"object","title":"Response Failed Ingestion Noise Count Api Cs Failed Ingestion Noise Count Get"}}}}}}},"/api/sales/playbooks":{"post":{"tags":["sales"],"summary":"Create Playbook","operationId":"create_playbook_api_sales_playbooks_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePlaybookRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Playbook Api Sales Playbooks Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sales"],"summary":"List Playbooks","operationId":"list_playbooks_api_sales_playbooks_get","parameters":[{"name":"segment","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Playbooks Api Sales Playbooks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}":{"get":{"tags":["sales"],"summary":"Get Playbook","operationId":"get_playbook_api_sales_playbooks__playbook_id__get","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Playbook Api Sales Playbooks  Playbook Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}/versions":{"post":{"tags":["sales"],"summary":"Publish Version","operationId":"publish_version_api_sales_playbooks__playbook_id__versions_post","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishVersionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publish Version Api Sales Playbooks  Playbook Id  Versions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}/activate":{"post":{"tags":["sales"],"summary":"Activate Playbook","operationId":"activate_playbook_api_sales_playbooks__playbook_id__activate_post","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivatePlaybookRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Activate Playbook Api Sales Playbooks  Playbook Id  Activate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}/allocation":{"post":{"tags":["sales"],"summary":"Set Playbook Allocation","description":"Frederick, 2026-09-06: \"why can't I type numbers and percentages of\nhow much outreach I want to go?\" Because the box on the page was\n`readOnly` and this route did not exist.","operationId":"set_playbook_allocation_api_sales_playbooks__playbook_id__allocation_post","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetAllocationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Playbook Allocation Api Sales Playbooks  Playbook Id  Allocation Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}/retire":{"post":{"tags":["sales"],"summary":"Retire Playbook","operationId":"retire_playbook_api_sales_playbooks__playbook_id__retire_post","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Retire Playbook Api Sales Playbooks  Playbook Id  Retire Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}/versions/{version_id}/approve-story":{"post":{"tags":["sales"],"summary":"Approve Story","description":"The human's item, once per version, not once per campaign\n(docs/03_modules/OUTREACH_CAMPAIGNS.md, \"Story approval\"). Gated on\n`sales:engine:write` -- the same grant `campaign_routes.approve_\ncampaign` uses -- because this is the same decision that used to be\nmade per campaign: who we are as a sender.","operationId":"approve_story_api_sales_playbooks__playbook_id__versions__version_id__approve_story_post","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Version Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveStoryRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Story Api Sales Playbooks  Playbook Id  Versions  Version Id  Approve Story Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/{playbook_id}/versions/{version_id}/examples":{"get":{"tags":["sales"],"summary":"List Version Examples","description":"What a human reads to approve the story: real renders, never sent,\nnever a sample (docs/03_modules/OUTREACH_CAMPAIGNS.md, \"Examples, not\na sample of ten\"). Read-only -- generating them is the POST below.","operationId":"list_version_examples_api_sales_playbooks__playbook_id__versions__version_id__examples_get","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Version Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Version Examples Api Sales Playbooks  Playbook Id  Versions  Version Id  Examples Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["sales"],"summary":"Generate Version Examples","description":"Renders the story's example set if none exists yet -- idempotent:\na second call against a version that already has examples just returns\nthem, it never re-renders (a human approving the story reads the SAME\nexamples the approval refers to).","operationId":"generate_version_examples_api_sales_playbooks__playbook_id__versions__version_id__examples_post","parameters":[{"name":"playbook_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Playbook Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Version Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Generate Version Examples Api Sales Playbooks  Playbook Id  Versions  Version Id  Examples Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/suppressions":{"get":{"tags":["sales"],"summary":"List Suppressions","operationId":"list_suppressions_api_sales_suppressions_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Suppressions Api Sales Suppressions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["sales"],"summary":"Suppress","operationId":"suppress_api_sales_suppressions_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Suppress Api Sales Suppressions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/sends":{"get":{"tags":["sales"],"summary":"List Sends","operationId":"list_sends_api_sales_sends_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"channel","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Sends Api Sales Sends Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["sales"],"summary":"Record Send","operationId":"record_send_api_sales_sends_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordSendRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Send Api Sales Sends Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/sends/{send_id}":{"get":{"tags":["sales"],"summary":"Get Send","operationId":"get_send_api_sales_sends__send_id__get","parameters":[{"name":"send_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Send Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Send Api Sales Sends  Send Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/nurture":{"get":{"tags":["sales"],"summary":"List Nurture","operationId":"list_nurture_api_sales_nurture_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Nurture Api Sales Nurture Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/nurture/demote":{"post":{"tags":["sales"],"summary":"Demote To Nurture","operationId":"demote_to_nurture_api_sales_nurture_demote_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Demote To Nurture Api Sales Nurture Demote Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/nurture/{org_id}/re-engage":{"post":{"tags":["sales"],"summary":"Re Engage From Nurture","operationId":"re_engage_from_nurture_api_sales_nurture__org_id__re_engage_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Re Engage From Nurture Api Sales Nurture  Org Id  Re Engage Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/signals":{"get":{"tags":["sales"],"summary":"List Signals","operationId":"list_signals_api_sales_signals_get","parameters":[{"name":"signal_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Signal Type"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Signals Api Sales Signals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["sales"],"summary":"Create Signal","operationId":"create_signal_api_sales_signals_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSignalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Signal Api Sales Signals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/signals/{signal_id}":{"get":{"tags":["sales"],"summary":"Get Signal","operationId":"get_signal_api_sales_signals__signal_id__get","parameters":[{"name":"signal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Signal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Signal Api Sales Signals  Signal Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/signals/{signal_id}/action":{"post":{"tags":["sales"],"summary":"Action Signal","operationId":"action_signal_api_sales_signals__signal_id__action_post","parameters":[{"name":"signal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Signal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Action Signal Api Sales Signals  Signal Id  Action Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/signals/{signal_id}/dismiss":{"post":{"tags":["sales"],"summary":"Dismiss Signal","operationId":"dismiss_signal_api_sales_signals__signal_id__dismiss_post","parameters":[{"name":"signal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Signal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Dismiss Signal Api Sales Signals  Signal Id  Dismiss Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/pacing":{"get":{"tags":["sales"],"summary":"List Pacing","operationId":"list_pacing_api_sales_pacing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Pacing Api Sales Pacing Get"}}}}}}},"/api/sales/pacing/{channel}":{"patch":{"tags":["sales"],"summary":"Set Pacing","operationId":"set_pacing_api_sales_pacing__channel__patch","parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","title":"Channel"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPacingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Pacing Api Sales Pacing  Channel  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/unsubscribe/tokens":{"post":{"tags":["sales"],"summary":"Mint Token","description":"Mints an unsubscribe token to embed in a cold send's footer. Behind\nsales:write -- a future real sender is the intended caller once one\nexists (see module docstring); exposed as a normal endpoint today so it\nis independently testable and usable ahead of that wiring.","operationId":"mint_token_api_sales_unsubscribe_tokens_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintUnsubscribeTokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Mint Token Api Sales Unsubscribe Tokens Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/unsubscribe/{token}":{"get":{"tags":["sales"],"summary":"Unsubscribe","description":"PUBLIC. No auth -- master spec 3.8's one-click handling means anyone\nholding the link can use it, by construction; the token itself is the\ncredential (32 bytes of secrets.token_urlsafe entropy). Idempotent -- see\nconsume_unsubscribe_token.","operationId":"unsubscribe_api_sales_unsubscribe__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/bounces":{"post":{"tags":["sales"],"summary":"Bounces","description":"SendGrid Event Webhook target: a JSON array of event objects (bounce,\ndropped, spamreport, unsubscribe, delivered, open, click, ...). Always\n200s on a well-formed POST once the secret checks out -- a non-200 here\nwould make SendGrid retry-storm a request whose events were already\ndurably recorded (see process_bounce_events).","operationId":"bounces_api_sales_bounces_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Bounces Api Sales Bounces Post"}}}}}}},"/api/sales/playbooks/author/converse":{"post":{"tags":["sales"],"summary":"Converse With Rooster","description":"Say something to Rooster about a playbook. Persists your turn and\nreturns.\n\nCalls no model. Rooster is a Goose agent on Frederick's own\nsubscription, dispatched by the local worker, so his reply lands on the\nconversation a moment later and the page polls for it. This used to hold\nthe request open across an Anthropic call and, with no key configured,\nanswered as a stub introducing itself as one.","operationId":"converse_with_rooster_api_sales_playbooks_author_converse_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__sales__authoring_routes__ConverseRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Converse With Rooster Api Sales Playbooks Author Converse Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/author/{conversation_id}":{"get":{"tags":["sales"],"summary":"Read Conversation","description":"The conversation as it stands, which is how the page learns Rooster\nhas answered. Yours only: an authoring conversation is the author's.","operationId":"read_conversation_api_sales_playbooks_author__conversation_id__get","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Conversation Api Sales Playbooks Author  Conversation Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/playbooks/author/{conversation_id}/confirm":{"post":{"tags":["sales"],"summary":"Confirm Publish","description":"Publishes the real playbook version from the conversation's current\ndraft -- the one place an authoring conversation ever produces a real\nversion, always an explicit human action, never automatic.","operationId":"confirm_publish_api_sales_playbooks_author__conversation_id__confirm_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Confirm Publish Api Sales Playbooks Author  Conversation Id  Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/signals/ingest/zoominfo":{"post":{"tags":["sales"],"summary":"Ingest Zoominfo","operationId":"ingest_zoominfo_api_sales_signals_ingest_zoominfo_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestSignalRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ingest Zoominfo Api Sales Signals Ingest Zoominfo Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/signals/ingest/bidprime":{"post":{"tags":["sales"],"summary":"Ingest Bidprime","operationId":"ingest_bidprime_api_sales_signals_ingest_bidprime_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestSignalRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ingest Bidprime Api Sales Signals Ingest Bidprime Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/pricing":{"get":{"tags":["sales"],"summary":"Get Lead Pricing","description":"Standard lead pricing's rate/minimum, for the live arithmetic preview\nLeadFormModal/EditLeadModal show as ADP is typed (build task\n\"standardised lead pricing\", 2026-08-09: \"Show the arithmetic on\nscreen\"). Gated on sales:read, not the Admin-only platform:\nconstitution:read the generic GET /api/platform/settings carries --\nKeith holds sales:read and needs this number; he does not hold (and\nshould not need) an Admin grant just to see what a lead is worth. Reads\nthrough the SAME settings_service accessors pipeline_service.\nderive_lead_est_value_cents uses, so this can never show a number the\nactual write path would not also use.","operationId":"get_lead_pricing_api_sales_pricing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Lead Pricing Api Sales Pricing Get"}}}}}}},"/api/sales/originators":{"get":{"tags":["sales"],"summary":"List Originators","description":"Who a lead may be attributed to, and what kind each is.\n\nBacks the \"where did this lead come from\" slot on the create form.\nDeliberately not the staff list: offering everybody is how a lead ends up\noriginated by whoever typed it in.\n\n`kind` is salesperson / partner / machine, and `note` says in plain words\nwhat a non-person one IS -- \"Rooster (went out and found it)\", \"Inbound\n(they came to us)\". Both machines share a `kind`, so the note is the only\nthing that tells them apart on screen, which is exactly the distinction\nFrederick asked for: \"one designation for Rooster going out and finding a\nlead, another designation for a lead finding us.\"\n\nNone of it decides a payment. Commission is computed from contract_terms\n(finance/commissions.py), which never reads lead attribution -- see 0237.\nA `house_account` boolean used to live here saying otherwise; it was a\nrestatement of `kind != salesperson` rendered as the literal words \"(house\naccount)\", and it is gone.","operationId":"list_originators_api_sales_originators_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Originators Api Sales Originators Get"}}}}}}},"/api/sales/leads":{"post":{"tags":["sales"],"summary":"Create Lead","operationId":"create_lead_api_sales_leads_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLeadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Lead Api Sales Leads Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sales"],"summary":"List Leads","operationId":"list_leads_api_sales_leads_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"stage","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"}},{"name":"owner_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"}},{"name":"include_converted","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Converted"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Leads Api Sales Leads Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/leads/{lead_id}":{"get":{"tags":["sales"],"summary":"Get Lead","operationId":"get_lead_api_sales_leads__lead_id__get","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Lead Api Sales Leads  Lead Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sales"],"summary":"Delete Lead","description":"Take a lead off the board.\n\nFrederick, 2026-08-21: \"I have no way to delete a lead. I should be able to\ndelete a lead. I'm an admin... I should be able to do it.\" There was no\nendpoint at all, so a board that could only be added to filled with test\nrows and mistakes, each one permanently in the denominator of every\npipeline number.\n\n`sales:write`, the same grant that creates and moves a lead, rather than a\nnew one. Somebody trusted to drag a lead into Disqualified and record a\nloss is trusted to remove one they should not have made; inventing a\nseparate permission for the smaller act would be ceremony, and Frederick's\nown framing was that an admin should simply be able to.\n\n204, no body: there is nothing left to return, and a caller that got a lead\nback would have to decide what to do with it.","operationId":"delete_lead_api_sales_leads__lead_id__delete","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["sales"],"summary":"Update Lead","description":"Edit a lead. `stage` is not editable here on purpose -- moving is\nPOST /leads/{id}/move, which enforces the path rules and writes a\nstage_changed event.\n\n⛔ATTRIBUTION IS GATED ABOVE `sales:write`. Frederick, 2026-08-22: \"Keith\nshould not be able to change the source, but I should be able to, and I\ncan correct anyone's that need to be corrected.\"\n\n`source` and `originated_by_principal_id` decide commission --\n`lead_originators` marks every non-salesperson originator as a house\naccount, so moving a lead between those buckets moves money. `sales:write`\nis what a salesperson must hold to work leads at all, so it cannot also be\nthe permission that decides who gets paid for them. Editing anything else\non the lead is unchanged.","operationId":"update_lead_api_sales_leads__lead_id__patch","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLeadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Lead Api Sales Leads  Lead Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/leads/{lead_id}/move":{"post":{"tags":["sales"],"summary":"Move Lead","operationId":"move_lead_api_sales_leads__lead_id__move_post","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveLeadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Move Lead Api Sales Leads  Lead Id  Move Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/leads/{lead_id}/disqualify":{"post":{"tags":["sales"],"summary":"Disqualify Lead","operationId":"disqualify_lead_api_sales_leads__lead_id__disqualify_post","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisqualifyLeadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Disqualify Lead Api Sales Leads  Lead Id  Disqualify Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/leads/{lead_id}/events":{"post":{"tags":["sales"],"summary":"Append Lead Event","operationId":"append_lead_event_api_sales_leads__lead_id__events_post","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppendLeadEventRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Append Lead Event Api Sales Leads  Lead Id  Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/leads/{lead_id}/next-step/complete":{"post":{"tags":["sales"],"summary":"Complete Next Step","description":"Mark this lead's current next step done -- the write\n`sales.next_steps_kept` reads (migration 0256). No request body: there\nis nothing to choose, only a moment to record, and that moment is always\n`now()` server-side (service.complete_next_step) so a caller cannot\nbackdate one.","operationId":"complete_next_step_api_sales_leads__lead_id__next_step_complete_post","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Complete Next Step Api Sales Leads  Lead Id  Next Step Complete Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/leads/{lead_id}/convert":{"post":{"tags":["sales"],"summary":"Convert Lead","operationId":"convert_lead_api_sales_leads__lead_id__convert_post","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lead Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertLeadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Convert Lead Api Sales Leads  Lead Id  Convert Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/deals":{"post":{"tags":["sales"],"summary":"Create Deal","operationId":"create_deal_api_sales_deals_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDealRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Deal Api Sales Deals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sales"],"summary":"List Deals","operationId":"list_deals_api_sales_deals_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"stage","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"}},{"name":"path","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path"}},{"name":"owner_principal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Deals Api Sales Deals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/deals/promote":{"post":{"tags":["sales"],"summary":"Promote Org To Deal","operationId":"promote_org_to_deal_api_sales_deals_promote_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteOrgToDealRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Promote Org To Deal Api Sales Deals Promote Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/deals/{deal_id}":{"get":{"tags":["sales"],"summary":"Get Deal","operationId":"get_deal_api_sales_deals__deal_id__get","parameters":[{"name":"deal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Deal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Deal Api Sales Deals  Deal Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["sales"],"summary":"Update Deal","description":"Edit a deal -- price and ADP above all. `stage` is not editable here:\nmoving is POST /deals/{id}/move, losing is /lose.\n\n⛔ATTRIBUTION IS GATED ABOVE `sales:write`, same as `update_lead` and the\nsame reason (F1, Fable's third review of #2345, 2026-09-26): who set up\nthe meeting decides commission (R-476/Q2), so the person paid on it must\nnot be the one who can rewrite it. Frederick, 2026-08-22: \"I can correct\nanyone's that need to be corrected\" -- this is the DEAL side of that; the\nlead route above already had it.","operationId":"update_deal_api_sales_deals__deal_id__patch","parameters":[{"name":"deal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Deal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDealRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Deal Api Sales Deals  Deal Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/deals/{deal_id}/move":{"post":{"tags":["sales"],"summary":"Move Deal","description":"Move a deal, and -- when the move is to `won` -- open the activation\nobligation in the SAME transaction (Stage E: \"the activation sequence\nfor a signed agency as one transaction\").\n\n⛔THIS ROUTE IS THE ONLY CALLER OF `service.move_deal` IN THE\nAPPLICATION (verified 2026-09-20), which is why the composition sits\nhere rather than inside the service. If a second caller ever appears,\nit inherits this obligation and must open it too -- a won deal with no\nactivation work is an agency nobody is activating, which is the exact\nfailure Stage E names. The service's own docstring carries the same\nwarning so the next writer does not have to find this one.\n\n⛔THE OWNER IS RESOLVED FROM CONFIGURATION AND NEVER GUESSED, and never\nthe acting human. `EXECUTIVE_MANAGER_PRINCIPAL_ID` is the same variable\n`tools/executive_engine/loop.py` already resolves the manager from, with\nits own documented reason: production carries four `kind='agent'`\nprincipals and there is no unambiguous \"the Rooster principal\" to look\nup. Unset means no obligation is opened AND THE RESPONSE SAYS SO\n(`activation: \"no_owner_configured\"`) -- a silent skip here would be a\nwon deal that looks activated.","operationId":"move_deal_api_sales_deals__deal_id__move_post","parameters":[{"name":"deal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Deal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveDealRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Move Deal Api Sales Deals  Deal Id  Move Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/orgs/{org_id}/activation-clock":{"get":{"tags":["sales"],"summary":"Activation Clock","description":"Days-to-first-mail for a signed agency (Stage E's clock).\n\n⛔`on_time` COMES BACK AS `null` WHEN NO TARGET IS SET, and the screen\nmust render that as \"no target\", never as a tick. See\n`settings_service.AGENCY_FIRST_MAIL_TARGET_DAYS`.","operationId":"activation_clock_api_sales_orgs__org_id__activation_clock_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Activation Clock Api Sales Orgs  Org Id  Activation Clock Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/meetings/{meeting_id}/hand-to-host":{"post":{"tags":["sales"],"summary":"Hand Meeting To Host","description":"Hand a booked meeting to the person who will sit in the room, with a\ndeadline, and KEEP the follow-through (Stage E; migration 0531's\n\"escalating does not transfer accountability\").\n\n⛔THE OWNER IS ROOSTER, NEVER THE HOST AND NEVER THE CALLER. Resolved\nfrom the same configured principal the activation obligation uses; unset\nmeans no obligation is opened and the response says so, because an\nobligation assigned to nobody is the thing this route exists to\nprevent.","operationId":"hand_meeting_to_host_api_sales_meetings__meeting_id__hand_to_host_post","parameters":[{"name":"meeting_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Meeting Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandMeetingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Hand Meeting To Host Api Sales Meetings  Meeting Id  Hand To Host Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/pricing-bands":{"get":{"tags":["sales"],"summary":"Pricing Band State","description":"What is actually pricing our leads right now.\n\n⛔THIS ROUTE EXISTS SO THE UNSET STATE IS VISIBLE. Stage E's instruction\nis to build the band mechanism and leave the values unset *visibly*; a\nband table that is empty and unmentioned is indistinguishable from one\nnobody has looked at. `pricing_basis` names which of the two rules is\nderiving lead values, and `awaiting` says whose numbers are missing.","operationId":"pricing_band_state_api_sales_pricing_bands_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pricing Band State Api Sales Pricing Bands Get"}}}}}}},"/api/sales/deals/{deal_id}/lose":{"post":{"tags":["sales"],"summary":"Lose Deal","operationId":"lose_deal_api_sales_deals__deal_id__lose_post","parameters":[{"name":"deal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Deal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoseDealRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Lose Deal Api Sales Deals  Deal Id  Lose Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/deals/{deal_id}/events":{"post":{"tags":["sales"],"summary":"Append Deal Event","operationId":"append_deal_event_api_sales_deals__deal_id__events_post","parameters":[{"name":"deal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Deal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppendDealEventRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Append Deal Event Api Sales Deals  Deal Id  Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/credentials":{"get":{"tags":["system"],"summary":"List Credentials","description":"Which keys are on file, and nothing about what they are.\n\n`storage_available` tells the page whether saving would work at all, so it\ncan say \"credential storage is not configured on this environment\"\ninstead of offering a field that 500s on submit.","operationId":"list_credentials_api_system_credentials_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Credentials Api System Credentials Get"}}}}}}},"/api/system/credentials/import-ticket":{"post":{"tags":["system"],"summary":"Create Import Ticket","description":"Start a one-click credential import. PUBLIC BY NECESSITY: the launcher on\nthe Mac has no session. All it gets is a ticket that does nothing until a\nholder of `system:credentials:write` presses Approve at `approve_url` (the\nPlaid card on System -> Integrations); the\nnumber of open tickets is capped (`import_tickets.MAX_OPEN_TICKETS`).\nFrederick, 2026-10-04 (see `import_tickets`).","operationId":"create_import_ticket_api_system_credentials_import_ticket_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Import Ticket Api System Credentials Import Ticket Post"}}}}}}},"/api/system/credentials/import-ticket/{ticket}":{"get":{"tags":["system"],"summary":"Read Import Ticket","description":"Poll a ticket's state (pending, approved, used, expired). PUBLIC: the\nticket is the credential (256 bits, never stored, only its hash).","operationId":"read_import_ticket_api_system_credentials_import_ticket__ticket__get","parameters":[{"name":"ticket","in":"path","required":true,"schema":{"type":"string","title":"Ticket"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Import Ticket Api System Credentials Import Ticket  Ticket  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["system"],"summary":"Redeem Import Ticket","description":"Write the ticket's credentials to the vault, once, and burn the ticket.\nPUBLIC: the approved ticket is the credential. Accepts exactly the vault\nnames of the ticket's scope; the writes are `service.set_credential`, the\nsame code path as the Integrations card. Only last-fours come back.","operationId":"redeem_import_ticket_api_system_credentials_import_ticket__ticket__put","parameters":[{"name":"ticket","in":"path","required":true,"schema":{"type":"string","title":"Ticket"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportTicketValues"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Redeem Import Ticket Api System Credentials Import Ticket  Ticket  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/credentials/import-approval/{approval_id}/info":{"get":{"tags":["system"],"summary":"Import Approval Info","operationId":"import_approval_info_api_system_credentials_import_approval__approval_id__info_get","parameters":[{"name":"approval_id","in":"path","required":true,"schema":{"type":"string","title":"Approval Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Import Approval Info Api System Credentials Import Approval  Approval Id  Info Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/credentials/import-approval/{approval_id}/approve":{"post":{"tags":["system"],"summary":"Import Approval Approve","description":"Approve the import. Only a holder of `system:credentials:write` reaches\nthis body (the route's gate); the approver is recorded and becomes the actor\non the credentials the launcher then writes.","operationId":"import_approval_approve_api_system_credentials_import_approval__approval_id__approve_post","parameters":[{"name":"approval_id","in":"path","required":true,"schema":{"type":"string","title":"Approval Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Import Approval Approve Api System Credentials Import Approval  Approval Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/credentials/{name}":{"put":{"tags":["system"],"summary":"Set Credential","operationId":"set_credential_api_system_credentials__name__put","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCredentialRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Credential Api System Credentials  Name  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["system"],"summary":"Delete Credential","operationId":"delete_credential_api_system_credentials__name__delete","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Credential Api System Credentials  Name  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/wise/probe":{"post":{"tags":["system"],"summary":"Probe Wise","description":"Prove the pasted Wise key actually works, on demand -- `WisePanel.tsx`\ncalls this right after a save, the same \"save then verify\" shape\n`/api/system/wise/probe` exists to give a person immediate feedback\nfor, rather than waiting on the next `/api/health` poll's cached result\n(`wise.PROBE_CACHE_TTL_SECONDS`).\n\nNo generic `POST /credentials/{name}/probe` route exists yet (nothing\nelse needed one), so this is Wise's own, gated exactly like every other\nSystem write. Never raises on a bad key -- `wise.probe_wise_key` is\nitself non-raising and records the verdict either way; this only\ntranslates that into what the panel shows.","operationId":"probe_wise_api_system_wise_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Wise Api System Wise Probe Post"}}}}}}},"/api/system/browserbase/probe":{"post":{"tags":["system"],"summary":"Probe Browserbase","description":"Prove the pasted Browserbase key actually works, on demand -- the\nsame \"save then verify\" shape `/api/system/wise/probe` gives Wise's own\ncard. `GET /v1/projects` only, never a session create (see\n`browserbase.py`'s own module docstring for why that distinction\nmatters here specifically). Never raises on a bad key --\n`browserbase.probe_browserbase_key` is itself non-raising and records\nthe verdict either way; this only translates that into what the panel\nshows.","operationId":"probe_browserbase_api_system_browserbase_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Browserbase Api System Browserbase Probe Post"}}}}}}},"/api/system/fireworks/probe":{"post":{"tags":["system"],"summary":"Probe Fireworks","description":"Prove the pasted Fireworks key actually works, on demand.\n\nFrederick, 2026-09-29 (via handoff `rooster-transcription-efficiency-\n2026-09-29.md`): seat 4 (Nemotron) fails 68% of transcription reads on\nNVIDIA's free tier. Fireworks is the trial candidate; this route is\nwhat `ApiKeyTestPanel.tsx`'s \"Test\" button calls right after a save --\nthe same \"save then verify\" shape `/api/system/wise/probe` gives Wise's\nown card. `GET /models` on Fireworks' inference host, never a priced\ncompletion. PASS/FAIL ONLY, per the build task: the panel shows\n\"Connected\" or \"Fireworks rejected this key\", nothing about which model\nthe trial will use (that is the trial tool's own job, not a credential\nprobe's).\n\nNever raises on a bad key -- `models.probe_fireworks_key` is itself\nnon-raising; this only records the verdict and translates it into what\nthe panel shows.","operationId":"probe_fireworks_api_system_fireworks_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Fireworks Api System Fireworks Probe Post"}}}}}}},"/api/system/supplier-accounts/{supplier_key}/probe":{"post":{"tags":["system"],"summary":"Probe Supplier Account","description":"\"Supplier accounts\" card, System -> Integrations -- docs/01_\ndecisions/rulings/2026-09-30-rooster-sources-parts.md. Logs IN only,\nnever navigates to a cart or checkout (Frederick's own words) --\n`equipment.part_sourcing.test_supplier_account_login`'s own docstring\nis the actual boundary; this route only translates its `bool | None`\ninto the same `{\"usable\": ...}` shape `ApiKeyTestPanel.tsx` already\nexpects from every other credential card's probe.","operationId":"probe_supplier_account_api_system_supplier_accounts__supplier_key__probe_post","parameters":[{"name":"supplier_key","in":"path","required":true,"schema":{"type":"string","title":"Supplier Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Probe Supplier Account Api System Supplier Accounts  Supplier Key  Probe Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/deepinfra/probe":{"post":{"tags":["system"],"summary":"Probe Deepinfra","description":"Prove the pasted DeepInfra key actually works, on demand -- same\n\"save then verify\" shape as `/api/system/fireworks/probe`, for the\nsecond seat-4 trial candidate. `GET /models`, never a priced\ncompletion, pass/fail only.","operationId":"probe_deepinfra_api_system_deepinfra_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Deepinfra Api System Deepinfra Probe Post"}}}}}}},"/api/system/vultr_inference/probe":{"post":{"tags":["system"],"summary":"Probe Vultr Inference","description":"Prove the pasted Vultr Serverless Inference key actually works, on\ndemand -- Frederick, 2026-09-29, created a \"Roost\" subscription there\nand asked for a credential slot \"exactly like the fireworks one\". Same\n\"save then verify\" shape as `/api/system/fireworks/probe`: `GET\n/v1/models`, never a priced completion, pass/fail only.","operationId":"probe_vultr_inference_api_system_vultr_inference_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Vultr Inference Api System Vultr Inference Probe Post"}}}}}}},"/api/system/google_gemini/probe":{"post":{"tags":["system"],"summary":"Probe Google Gemini","description":"Prove the pasted Google Gemini (Google AI Studio) key actually works,\non demand -- Frederick, 2026-09-29, after a 20-envelope OpenRouter trial\nof Gemini 3.5 Flash-Lite (name 85% / ID 70% / facility 94%, 0% failures,\n~$0.0014/envelope): \"i will go direct to google\" / \"make a card for\nit\". Same \"save then verify\" shape as `/api/system/fireworks/probe`:\n`GET /v1beta/models` with `x-goog-api-key`, never a priced completion,\npass/fail only. See `docs/01_decisions/TRANSCRIPTION_READER_FLEET.md`.","operationId":"probe_google_gemini_api_system_google_gemini_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Google Gemini Api System Google Gemini Probe Post"}}}}}}},"/api/system/gcp_fulfillment_reader":{"get":{"tags":["system"],"summary":"Get Gcp Fulfillment Reader Identity","description":"Which service account these GCS reads run AS -- the read-only card\nthis replaced the paste card with (R-2026-09-30, Frederick: \"Yes,\nlet's do that. Keep it simple.\"). Names the email only, never key\nmaterial -- same \"principal, never secret\" rule the probe's own alert\nalready follows (`document_resolver._reader_principal_email`).","operationId":"get_gcp_fulfillment_reader_identity_api_system_gcp_fulfillment_reader_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Gcp Fulfillment Reader Identity Api System Gcp Fulfillment Reader Get"}}}}}}},"/api/system/gcp_fulfillment_reader/probe":{"post":{"tags":["system"],"summary":"Probe Gcp Fulfillment Reader","description":"Prove the GCS reads actually work -- issue #2608, unit 8. Same \"save\nthen verify\" shape as `/api/system/google_gemini/probe`, adapted for a\ncredential that mints its own bearer token rather than carrying one\ndirectly: mints a JWT-bearer token (proving the Workspace key is\nwell-formed and Google accepts the assertion) and, when at least one\nbucket is on the allowlist, also reads THAT bucket's own metadata\n(proving the IAM grant too).\n\n⛔BUCKET METADATA ONLY, NEVER AN OBJECT READ (unit 8's own rule) --\nsee `gcs_reader.probe_credential`'s own docstring.\n\n⛔CORRECTED 2026-09-30: this used to also record the verdict against a\nseparate `gcp_fulfillment_reader` vault row (`service.\nrecord_credential_probe_result`). That row no longer exists -- the\ncredential probed here is the reused Workspace service account, which\nis never pasted through this page -- so there is nothing left to\nrecord a probe result against; the verdict is returned to the card\ndirectly and nowhere else, same as before this correction from the\ncaller's point of view.","operationId":"probe_gcp_fulfillment_reader_api_system_gcp_fulfillment_reader_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Gcp Fulfillment Reader Api System Gcp Fulfillment Reader Probe Post"}}}}}}},"/api/system/rooster-browser-anti-bot":{"get":{"tags":["system"],"summary":"Get Rooster Browser Anti Bot","description":"Whether Rooster's Browserbase sessions use its anti-bot features\n(Verified browser, residential proxies, CAPTCHA solving) on Pigeonly's\nown vendor accounts. Read-gated the same as reading a credential's\npresence (`system:credentials:read`) -- seeing the switch's position is\nnot the sensitive act; changing it is (see `PUT`, below, gated on the\nnarrower owner-only grant).\n\n⛔#2334. `recent_sessions` is what actually ran, not only what the\nswitch is currently set to -- Frederick: \"Record the setting's value\non every browser-session ledger row.\" Each row's own `anti_bot_\nenabled` is the value THAT session used, observed at the moment it\nopened, so a switch flipped since is visible as a change partway down\nthe list rather than silently rewriting history.","operationId":"get_rooster_browser_anti_bot_api_system_rooster_browser_anti_bot_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Rooster Browser Anti Bot Api System Rooster Browser Anti Bot Get"}}}}}},"put":{"tags":["system"],"summary":"Set Rooster Browser Anti Bot","description":"Frederick, 2026-09-24: \"only the owner ... can turn on\" (default\nlater moved to ON, the gate did not -- see migration 0619's own\ncomment). `ANTI_BOT_MANAGE_KEY` is `system:browser_anti_bot:manage`,\nseeded to Frederick's principal alone; the route gate is the grantable\n`system:credentials:write`, and anyone without the owner key gets 403 from\nthe check below before anything is written. `settings_service.upsert_setting` records\nwho and when (`updated_by`/`updated_at`) and emits the same outbox\naudit event every other setting change already does -- no second audit\nmechanism needed here.","operationId":"set_rooster_browser_anti_bot_api_system_rooster_browser_anti_bot_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetAntiBotRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Set Rooster Browser Anti Bot Api System Rooster Browser Anti Bot Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/floor-scheduling-live":{"get":{"tags":["system"],"summary":"Get Floor Scheduling Live","description":"CHECKLIST.md (09-30 handoff), \"Settings: ... add a control for\n`FLOOR_SCHEDULING_LIVE`\": the one place this switch had no UI at all.\nRead-gated the same as the anti-bot switch above -- seeing the\nposition is not the sensitive act.","operationId":"get_floor_scheduling_live_api_system_floor_scheduling_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Floor Scheduling Live Api System Floor Scheduling Live Get"}}}}}},"put":{"tags":["system"],"summary":"Set Floor Scheduling Live","description":"Frederick's own call, by his own words -- see\n`FLOOR_SCHEDULING_MANAGE_KEY`'s own comment above. 403s for anyone but\nFrederick's principal; this route's own UI shows that refusal plainly\nrather than hiding the control (same discipline as the anti-bot\ntoggle).","operationId":"set_floor_scheduling_live_api_system_floor_scheduling_live_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFloorSchedulingLiveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Set Floor Scheduling Live Api System Floor Scheduling Live Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/gitlab/ensure-hook":{"post":{"tags":["system"],"summary":"Ensure Gitlab Hook","description":"Register the ONE GitLab group webhook `pigeonly` needs (docs/\n01_decisions/MARCS_ISSUES_GO_TO_MARCS_GITLAB.md, unit E4). Called from\nexactly this button on the System page's GitLab card, never from a\nrequest path -- `gitlab_issues.create_group_hook` has no upsert on\nGitLab's side, so calling it twice would register two hooks, and this\nroute's own idempotency (below) is what stands in for one.\n\nIDEMPOTENT ON THE STORED SECRET, not on asking GitLab. Once a webhook\nsecret exists (`gitlab_webhook.WEBHOOK_SECRET_CREDENTIAL_NAME`), this\nreturns `already_configured` without calling GitLab again -- the secret\nIS the evidence a hook was already created through this exact button,\nsince nothing else in this codebase ever writes that credential name.\n\nReturns `stub` (never calls GitLab) when no `gitlab` API credential is\nconfigured yet -- same \"the build never stalls on a missing key\"\nposture every other adapter in this codebase holds itself to.\n\nThe secret is returned in this response ONLY on the run that generates\nit -- there is nowhere to read a stored secret back out afterward\n(same as every other credential on this page), so a person who misses\nit here has to press the button again on a fresh secret... except this\nbutton will not press again once one is stored. If GitLab's own hook\never needs re-pointing, delete `gitlab_webhook_secret` first through\n`DELETE /api/system/credentials/gitlab_webhook_secret`, then press this\nagain.","operationId":"ensure_gitlab_hook_api_system_gitlab_ensure_hook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ensure Gitlab Hook Api System Gitlab Ensure Hook Post"}}}}}}},"/api/system/gitlab/principal-mapping":{"put":{"tags":["system"],"summary":"Set Gitlab Username","description":"The one-time step for mapping the NEXT GitLab username (F10): a\nperson with `system:credentials:write` picks the Roost principal and\ntypes the `gitlab.com` username GitLab itself shows as that person's\nassignee -- never guessed, never inferred from a name.\n\nRefuses (422) a principal that is not `kind = 'human'` or is\n`removed_at IS NOT NULL` -- `app.modules.issues.gitlab_watch` would\nnever resolve either kind of row anyway (`_mapped_principals`,\n`_resolve_assignees` both filter on exactly this), so accepting the\nwrite and having it silently do nothing is worse than refusing it here\nwith a reason. Refuses (409) a username already mapped to a DIFFERENT\nprincipal -- `principals_gitlab_username_unique_idx` (migration 0642)\nis the floor under this; this route's own read-before-write check is\nwhat turns that floor into a message a person can act on instead of a\nbare `UniqueViolationError`.","operationId":"set_gitlab_username_api_system_gitlab_principal_mapping_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetGitLabUsernameRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Set Gitlab Username Api System Gitlab Principal Mapping Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/gitlab/registry":{"get":{"tags":["system"],"summary":"Read Gitlab Registry","description":"The System page's Integrations card (docs/01_decisions/\nMARCS_ISSUES_GO_TO_MARCS_GITLAB.md, ruling 2026-09-05, §6): \"shows the\nregistry, webhook state, token expiry date, and open upstream issues\nby project.\"\n\nREPORTS BELIEF, NOT LIVE TRUTH -- same posture `read_fleet_state`\nabove already takes, for the identical reason: this must stay usable\nexactly when GitLab itself is unreachable or rate-limiting us, which\nis precisely when a person most wants to see it. `token_expires_at`\nis `integration_credentials.external_expires_at` (migration 0357),\nrefreshed by `gitlab_issues.rotate_token_if_expiring` on the audit\ncadence -- never a live call to GitLab from this route.","operationId":"read_gitlab_registry_api_system_gitlab_registry_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Read Gitlab Registry Api System Gitlab Registry Get"}}}}}}},"/api/system/credentials/{name}/secret":{"get":{"tags":["system"],"summary":"Read Credential Secret","description":"The key itself. FOR SERVICES ONLY.\n\nRooster calls this at boot to get the Moonshot key, which is the whole\npoint of moving credentials out of environment variables: pasting a new\nkey into the System page has to actually reach the thing that uses it.\n\nGated on `system:credentials:read`, which migration 0091 binds to no human\nrole. If a person can reach this endpoint, someone has granted them the\nfunction on purpose.","operationId":"read_credential_secret_api_system_credentials__name__secret_get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Credential Secret Api System Credentials  Name  Secret Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/github-app/probe":{"post":{"tags":["system"],"summary":"Probe Github App","description":"Prove the pasted GitHub App credentials actually work, on demand --\nthe same \"save then verify\" shape `/api/system/wise/probe` and\n`/api/system/browserbase/probe` already give their own cards. Mints an\ninstallation token and lists the App's visible repositories, checking\nfor `PIGEONLY/roost-v2` -- the exact proof issue #2488 asked for\n(\"a 'test' that mints an installation token and calls `GET\n/installation/repositories` proving it sees roost-v2\"). Never raises --\n`github_app.probe_github_app` is itself non-raising and records the\nverdict either way; this only translates that into what the panel\nshows, pass/fail, never a stack trace.","operationId":"probe_github_app_api_system_github_app_probe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probe Github App Api System Github App Probe Post"}}}}}}},"/api/system/github-app/installation-token":{"get":{"tags":["system"],"summary":"Read Github App Installation Token","description":"A freshly minted (or cached, still-live) GitHub App installation\ntoken -- FOR SERVICES ONLY, and only services that speak to GitHub on\nthis build's behalf: the `rooster` production service (pushing fix\nbranches, opening/merging pull requests, filing issues -- `apps/\nrooster/entrypoint.sh`, `apps/api/tools/rooster_worker/drain.py`) and\n`roost-ci`'s ci-service (posting the `railway-ci/verdict` commit\nstatus, a separate Railway project with no direct database access of\nits own). Both replace a copy of the same exposed personal access\ntoken (issue #2488) with a token that is worthless again within the\nhour.\n\nGated on `system:github_app:mint_token`, not `system:credentials:read`\n-- see that dependency's own comment for why a second, narrower key.\n404 when the slot is not configured yet (neither credential pasted),\n502 when GitHub itself refused the mint -- both are the caller's cue to\nfall back to its own static token, logging loudly that it did.\n\nISS-1996: a mint waits on GitHub for up to ten seconds, and callers behind\nthe mint lock wait on that one, so the route holds no pool connection: each\nread takes a slot and gives it back before the call goes out.","operationId":"read_github_app_installation_token_api_system_github_app_installation_token_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Read Github App Installation Token Api System Github App Installation Token Get"}}}}}}},"/api/system/codex-seat/login":{"put":{"tags":["system"],"summary":"Put Codex Seat Login","description":"Write back the rooster container's refreshed ChatGPT login (BAKE4-2).\n\nFOR THE `rooster` SERVICE ONLY. The Codex CLI rotates the refresh token in\n`auth.json` on every refresh and that container has no volume, so the newest\ncopy is kept in the vault as `codex_auth_json` and restored at start\n(`apps/rooster/codex_seat_vault.py`). Stored only when STRICTLY newer, under\na database lock. Answers `{\"status\": \"written\" | \"unchanged\" | \"stale\"}`\n(`stale`: the vault's copy is newer, adopt it); 422 for a body that is not a\nChatGPT login; 503 without a vault key. The login text is never returned or\nlogged.","operationId":"put_codex_seat_login_api_system_codex_seat_login_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodexSeatLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Put Codex Seat Login Api System Codex Seat Login Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/deploy-events":{"post":{"tags":["system"],"summary":"Record Deploy Event","description":"Called by `scripts/promote-to-production.sh` (via\n`scripts/lib/record_deploy_event.py`) and by\n`tools/rooster_worker/drain.py`'s `deploy_main_to_staging`/\n`promote_pass` -- see each caller's own comment for why the write is\nbest-effort there (a promotion must never be blocked on this ledger\nbeing reachable). This route itself is a plain gated write: a bad\n`environment` is the one thing it refuses.","operationId":"record_deploy_event_api_system_deploy_events_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordDeployEventRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Deploy Event Api System Deploy Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["system"],"summary":"List Deploy Events","description":"The System page's own read of the ledger -- gated the same as every\nother read on this page (`system:credentials:write`; seeing this list is\nnot the sensitive act, same posture `list_credentials` above already\ntakes for its own page). Never the raw-secret read key: nothing here is\na secret.","operationId":"list_deploy_events_api_system_deploy_events_get","parameters":[{"name":"environment","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Deploy Events Api System Deploy Events Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/pipeline-findings":{"post":{"tags":["system"],"summary":"Report Pipeline Finding","description":"The deployer's watchdog (`ops/railway-ci/deployer/watchdog.py`,\nrelease-path unit U6, design section 3.7) reports a release-pipeline\ncondition it noticed on its 2-minute tick -- a dead fixer, a red main,\nstaging or production falling behind, a stalled CI queue, a stale\nimage build, ci-service drift, a ready pull request sitting unmerged,\na stranded branch, `PROMOTE_ALLOW_RED_CI` used.\n\nDeliberately the SAME channel Rooster's own findings already use\n(`app.modules.rooster.supervision.record_finding`,\n`POST /api/rooster/supervision/report`), not a second one: the System\npage and Frederick's Slack DMs already read that one card/DM path, and\na release-pipeline finding is exactly the same shape -- an observed\ncondition, deduped by `reference` so a still-red main is one open\ncard, not one per 2-minute tick, and cleared automatically the next\ntime supervision.record_resolution sees the reference reported\nhealthy.\n\nGated on `system:pipeline_findings:write` (migration 202609301620,\nsame two-part pattern #2515 used for the GitHub App token mint), not\non `rooster:supervision:write` -- the deployer's principal is not\nRooster's own pipeline principal and should not inherit whatever else\nthat grant happens to cover.","operationId":"report_pipeline_finding_api_system_pipeline_findings_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineFindingRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Report Pipeline Finding Api System Pipeline Findings Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/pipeline-findings/resolve":{"post":{"tags":["system"],"summary":"Resolve Pipeline Finding","description":"Added 2026-10-01 (`docs/01_decisions/DEV_CYCLE_2026_10_01.md`): the\ndeployer could OPEN a card through `report_pipeline_finding` above but\nhad no way to close one, so every watchdog card (\"main has been red\",\n\"production is behind a promotable sha\") stayed open on the desk after\nthe condition had cleared -- the exact \"alerted, never confirmed\" shape\n`supervision.record_resolution` exists to end. Same grant as the\nreport route (`system:pipeline_findings:write`): clearing a card is the\nother half of the same conversation, not a second permission.\n\nReturns `{\"resolved\": false}` when nothing was open under `reference`\n(a healthy tick with nothing outstanding is the common case and must\nstay silent -- `record_resolution` returns None and posts nothing).","operationId":"resolve_pipeline_finding_api_system_pipeline_findings_resolve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineFindingResolutionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Resolve Pipeline Finding Api System Pipeline Findings Resolve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/models":{"get":{"tags":["system"],"summary":"List Model Assignments","operationId":"list_model_assignments_api_system_models_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Model Assignments Api System Models Get"}}}}}}},"/api/system/seat-usage":{"get":{"tags":["system"],"summary":"Usage Summary","description":"The System page's \"Claude Code seats\" panel (RA-45's own Known-gaps\nrow, closed here): per seat, today and the last 7 days -- builds\nstarted/finished/failed, session minutes, rate-limit hits -- plus the\ncurrent queue depth per lane. See `app.modules.rooster.seat_usage` for\nwhere each number comes from.","operationId":"usage_summary_api_system_seat_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Usage Summary Api System Seat Usage Get"}}}}}}},"/api/system/models/{surface}":{"put":{"tags":["system"],"summary":"Set Model Pin","operationId":"set_model_pin_api_system_models__surface__put","parameters":[{"name":"surface","in":"path","required":true,"schema":{"type":"string","title":"Surface"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetModelPinRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Model Pin Api System Models  Surface  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/feature-flags":{"get":{"tags":["system"],"summary":"List Feature Flags","operationId":"list_feature_flags_api_system_feature_flags_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Feature Flags Api System Feature Flags Get"}}}}}}},"/api/system/feature-flags/{name}":{"put":{"tags":["system"],"summary":"Set Feature Flag","operationId":"set_feature_flag_api_system_feature_flags__name__put","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFeatureFlagRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Feature Flag Api System Feature Flags  Name  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/not-a-ticket-senders":{"get":{"tags":["system"],"summary":"List Not A Ticket Senders","operationId":"list_not_a_ticket_senders_api_system_not_a_ticket_senders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Not A Ticket Senders Api System Not A Ticket Senders Get"}}}}}}},"/api/system/not-a-ticket-senders/{sender_id}":{"delete":{"tags":["system"],"summary":"Remove Not A Ticket Sender","operationId":"remove_not_a_ticket_sender_api_system_not_a_ticket_senders__sender_id__delete","parameters":[{"name":"sender_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Sender Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Not A Ticket Sender Api System Not A Ticket Senders  Sender Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/dora":{"get":{"tags":["system"],"summary":"Get Dora Report","operationId":"get_dora_report_api_system_dora_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Dora Report Api System Dora Get"}}}}}}},"/api/system/bug-trends":{"get":{"tags":["system"],"summary":"Get Bug Trends","operationId":"get_bug_trends_api_system_bug_trends_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":14,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Bug Trends Api System Bug Trends Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/system/latency":{"get":{"tags":["system"],"summary":"Get Route Latency","description":"p50/p95/p99, request count, 5xx count and the per-request database\nwork (statements, DB ms, pool wait) for every API route over the last\n`hours` (1 to 720), slowest p95 first, each with the p95 budget it is\nheld to (`request_timing.budget_for`, null when none). Measured by the\nAPI itself (`app/platform/request_timing.py`), merged across every\nprocess.","operationId":"get_route_latency_api_system_latency_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Route Latency Api System Latency Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/engine/settings":{"get":{"tags":["sales"],"summary":"Get Engine Settings","operationId":"get_engine_settings_api_sales_engine_settings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Engine Settings Api Sales Engine Settings Get"}}}}}},"patch":{"tags":["sales"],"summary":"Update Engine Settings","operationId":"update_engine_settings_api_sales_engine_settings_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEngineSettingsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Update Engine Settings Api Sales Engine Settings Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/engine/week-stats":{"get":{"tags":["sales"],"summary":"Get Engine Week Stats","description":"The Engine screen's \"This week\" stat row -- B4B walk finding #2. See\nengine_service.get_week_stats's docstring for exactly what each count\nmeasures.","operationId":"get_engine_week_stats_api_sales_engine_week_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Engine Week Stats Api Sales Engine Week Stats Get"}}}}}}},"/api/sales/engine/originator-breakdown":{"get":{"tags":["sales"],"summary":"Get Originator Breakdown","description":"Leads and deals by originator (migration 0157) -- Frederick,\n2026-08-10: \"the moment we turn the machine on, we should see with those\nleads and deals that come from different places.\" `since`/`until` are\nboth optional (open window, i.e. all-time when neither is given -- see\nengine_service.get_originator_breakdown's own docstring for why).","operationId":"get_originator_breakdown_api_sales_engine_originator_breakdown_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"}},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Until"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Originator Breakdown Api Sales Engine Originator Breakdown Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/accounts/{org_id}/ladder":{"get":{"tags":["sales"],"summary":"Get Ladder","description":"How far up this account we have climbed, and who is next.\n\n`next_target` is null with a `no_target_reason` rather than an error: an\naccount with nobody left to write to is a normal state the screen has to\nshow, and the reason is the whole point -- \"account budget spent\" and\n\"no contact here has an email address\" call for different human action.","operationId":"get_ladder_api_sales_accounts__org_id__ladder_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Ladder Api Sales Accounts  Org Id  Ladder Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/accounts/{org_id}/ladder/brief":{"get":{"tags":["sales"],"summary":"Get Brief","description":"Everything needed to choose an approach at this account.\n\nThe goal is stated outright; everything else is evidence, including what\nhas actually worked on people at this rung. `suggested_play` is the\ndeterministic default, not an instruction.","operationId":"get_brief_api_sales_accounts__org_id__ladder_brief_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Brief Api Sales Accounts  Org Id  Ladder Brief Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/contacts/{contact_id}/authority":{"post":{"tags":["sales"],"summary":"Record Learned Authority","description":"They told us what they can decide. Outranks the title, survives sweeps.\n\nThe evidence is required and is their own sentence: when the reading is\nwrong, that is what a human corrects it from.","operationId":"record_learned_authority_api_sales_contacts__contact_id__authority_post","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnedAuthorityRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Learned Authority Api Sales Contacts  Contact Id  Authority Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/accounts/{org_id}/ladder/classify":{"post":{"tags":["sales"],"summary":"Classify Ladder","description":"Re-derive every contact's rung at this account from their title.\n\nRe-runnable on purpose: when a classifier pattern is fixed, this is what\ncarries the fix onto the rows it was written for.","operationId":"classify_ladder_api_sales_accounts__org_id__ladder_classify_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Classify Ladder Api Sales Accounts  Org Id  Ladder Classify Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/accounts/{org_id}/ladder/close":{"post":{"tags":["sales"],"summary":"Close Ladder","description":"Stop working an account. The reason is required and is stored on the row.","operationId":"close_ladder_api_sales_accounts__org_id__ladder_close_post","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloseAccountRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Close Ladder Api Sales Accounts  Org Id  Ladder Close Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/referrals/pending":{"get":{"tags":["sales"],"summary":"List Pending Referrals","description":"Referrals waiting on a human, oldest first.\n\nThis is the queue that makes the ladder move: a parsed name sits here doing\nnothing until somebody agrees it is real.","operationId":"list_pending_referrals_api_sales_referrals_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Pending Referrals Api Sales Referrals Pending Get"}}}}}}},"/api/sales/referrals":{"post":{"tags":["sales"],"summary":"Create Referral","description":"Somebody pointed us at somebody else -- typed in by a human here, parsed\nout of a reply by Rooster elsewhere. Both land as 'pending'.\n\nReturns `duplicate` rather than an error when this name is already an open\nreferral at this account: two people naming the same warden is one rung, and\nthe second person to say it has not done anything wrong.","operationId":"create_referral_api_sales_referrals_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordReferralRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Referral Api Sales Referrals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/referrals/{referral_id}/confirm":{"post":{"tags":["sales"],"summary":"Confirm Referral","description":"A human agrees the referral is real. Creates the contact and returns it.\n\nThe confirming principal is recorded, not inferred: migration 0261's\n`contact_referrals_settled_by_a_human` CHECK refuses a confirmed row that\ncannot name who confirmed it.","operationId":"confirm_referral_api_sales_referrals__referral_id__confirm_post","parameters":[{"name":"referral_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Referral Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Confirm Referral Api Sales Referrals  Referral Id  Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/referrals/{referral_id}/overturn":{"post":{"tags":["sales"],"summary":"Overturn Referral","description":"A human says the parse was wrong. The row stays, for the audit trail.","operationId":"overturn_referral_api_sales_referrals__referral_id__overturn_post","parameters":[{"name":"referral_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Referral Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Overturn Referral Api Sales Referrals  Referral Id  Overturn Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/outreach/stats":{"get":{"tags":["sales"],"summary":"Get Outreach Stats","operationId":"get_outreach_stats_api_sales_outreach_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Outreach Stats Api Sales Outreach Stats Get"}}}}}}},"/api/sales/outreach/campaigns":{"get":{"tags":["sales"],"summary":"Get Campaign Rollup","operationId":"get_campaign_rollup_api_sales_outreach_campaigns_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Campaign Rollup Api Sales Outreach Campaigns Get"}}}}}}},"/api/sales/outreach/linkedin":{"get":{"tags":["sales"],"summary":"Get Linkedin Panel","operationId":"get_linkedin_panel_api_sales_outreach_linkedin_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Linkedin Panel Api Sales Outreach Linkedin Get"}}}}}}},"/api/sales/outreach/context/{org_id}":{"get":{"tags":["sales"],"summary":"Get Outreach Context","description":"Everything Rooster needs to decide what to say next to one agency.\n\n⛔THE ROUTE THAT MAKES `agent_outreach.build_outreach_context` MORE THAN A\nMODULE. It already has a caller inside the API (`campaign_worker`'s\nautomated composition), but Rooster reads over HTTP as a chat-based\nreasoner, and without this route the brief was unreachable to him: the\nsame \"built but not running\" state that left `am_capture` without a\nscheduler and `account-watch` without a watcher.\n\nReturns facts and boundaries, never a decision: no ranked option, no chosen\ntemplate, no score. The judgement is Rooster's, and putting it back into\nPython would defeat the point of the brief.\n\n⛔SEALED TO ONE AGENCY BY THE PATH ITSELF. `org_id` is a path parameter\nrather than a filter, so there is no version of this call that reads across\nagencies. `build_outreach_context` raises `BlankOrgId` rather than\ndegrading, and the response carries a `provenance` block naming what is\nsealed and what is shared.","operationId":"get_outreach_context_api_sales_outreach_context__org_id__get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}},{"name":"contact_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Outreach Context Api Sales Outreach Context  Org Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/outreach/track-record":{"get":{"tags":["sales"],"summary":"Get Track Record","description":"What has actually worked, measured from sends rather than assumed.\n\nShared across agencies on purpose: which rung replies is a fact about the\nmarket, not about anyone's account, and it carries no agency's words.\n\nReads as empty while it is empty. Every rate is null until there is\nsomething to divide by, so a play that has never been tried cannot be\nmistaken for one that never works.","operationId":"get_track_record_api_sales_outreach_track_record_get","parameters":[{"name":"segment","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Track Record Api Sales Outreach Track Record Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/drafts/{draft_id}/feedback":{"post":{"tags":["sales"],"summary":"Submit Draft Feedback","description":"Frederick, 2026-09-06: \"I don't want to approve every email. I want\nyou to learn how to write from my feedback, and let Sonnet follow the\nlearnings.\" One correction, one lesson, scoped to the playbook -- never\nto the one email it was read on.","operationId":"submit_draft_feedback_api_sales_drafts__draft_id__feedback_post","parameters":[{"name":"draft_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Draft Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftFeedbackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Submit Draft Feedback Api Sales Drafts  Draft Id  Feedback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/lessons":{"get":{"tags":["sales"],"summary":"List Lessons","operationId":"list_lessons_api_sales_lessons_get","parameters":[{"name":"playbook_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Playbook Id"}},{"name":"include_retired","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Retired"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Lessons Api Sales Lessons Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/composer-length-refusal-rate":{"get":{"tags":["sales"],"summary":"Composer Length Refusal Rate","description":"How often the composer is still writing a draft the lint has to\nrefuse for length, over the trailing `window_days`. `rate` is null,\nwith `reason` stating why, when nothing was composed in the window --\nnever a bare 0 standing in for \"unmeasured\" (CLAUDE.md's own \"a blank\nor zero is a lie\").","operationId":"composer_length_refusal_rate_api_sales_composer_length_refusal_rate_get","parameters":[{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Window Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Composer Length Refusal Rate Api Sales Composer Length Refusal Rate Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/lessons/{lesson_id}/retire":{"post":{"tags":["sales"],"summary":"Retire Lesson","description":"Retiring is a click; nothing is deleted -- `retired_at` just excludes\nit from the next compose's active set.","operationId":"retire_lesson_api_sales_lessons__lesson_id__retire_post","parameters":[{"name":"lesson_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Lesson Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Retire Lesson Api Sales Lessons  Lesson Id  Retire Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/readiness":{"get":{"tags":["sales"],"summary":"Get Readiness","description":"The production checklist as data\n(docs/03_modules/OUTREACH_CAMPAIGNS.md): each item is `{key, ok, detail}`.\n`blocking` is true when any item OTHER than `bounce_webhook_secret` (a\nwarning, never a blocker) is not ok -- the Campaigns page reads this to\ndecide whether Create is even offered.","operationId":"get_readiness_api_sales_campaigns_readiness_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Readiness Api Sales Campaigns Readiness Get"}}}}}}},"/api/sales/campaigns":{"get":{"tags":["sales"],"summary":"List Campaigns","operationId":"list_campaigns_api_sales_campaigns_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Campaigns Api Sales Campaigns Get"}}}}}}},"/api/sales/campaigns/{campaign_id}":{"get":{"tags":["sales"],"summary":"Get Campaign Detail","operationId":"get_campaign_detail_api_sales_campaigns__campaign_id__get","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Campaign Detail Api Sales Campaigns  Campaign Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/start-sampling":{"post":{"tags":["sales"],"summary":"Start Sampling","operationId":"start_sampling_api_sales_campaigns__campaign_id__start_sampling_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Start Sampling Api Sales Campaigns  Campaign Id  Start Sampling Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/drafts":{"get":{"tags":["sales"],"summary":"List Drafts","operationId":"list_drafts_api_sales_campaigns__campaign_id__drafts_get","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}},{"name":"purpose","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purpose"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Drafts Api Sales Campaigns  Campaign Id  Drafts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/drafts/{draft_id}/decide":{"post":{"tags":["sales"],"summary":"Decide Sample","description":"A human approves or rejects one sample draft. A rejection REQUIRES a\nnote -- migration 0331's own CHECK constraint, enforced here first so\nthe caller gets the exact reason rather than a raw DB error.","operationId":"decide_sample_api_sales_campaigns__campaign_id__drafts__draft_id__decide_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}},{"name":"draft_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Draft Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideSampleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decide Sample Api Sales Campaigns  Campaign Id  Drafts  Draft Id  Decide Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/approve":{"post":{"tags":["sales"],"summary":"Approve Campaign","description":"awaiting_approval -> running, or 409 with the exact unmet rule. The\none decision that is Frederick's own -- who we are as a sender.","operationId":"approve_campaign_api_sales_campaigns__campaign_id__approve_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Campaign Api Sales Campaigns  Campaign Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/pause":{"post":{"tags":["sales"],"summary":"Pause Campaign","operationId":"pause_campaign_api_sales_campaigns__campaign_id__pause_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReasonRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pause Campaign Api Sales Campaigns  Campaign Id  Pause Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/resume":{"post":{"tags":["sales"],"summary":"Resume Campaign","description":"Un-pause, voiding any queued draft whose playbook, policy or pricing\nhas moved since it was written (Stage E).\n\n⛔THIS ROUTE CALLS `campaign_relaunch.resume`, NOT `service.resume_\ncampaign`. The service function is the plain status flip and is still the\nright primitive; what a human clicking Resume means is \"start sending\nagain\", and starting to send again with ten drafts written under\nsuperseded rules at the front of the queue is not what they asked for.\nThe count of what was voided comes back in the response so the click has\na receipt rather than a silence.","operationId":"resume_campaign_api_sales_campaigns__campaign_id__resume_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resume Campaign Api Sales Campaigns  Campaign Id  Resume Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/relaunch":{"post":{"tags":["sales"],"summary":"Relaunch Campaign","description":"Cancel a campaign pinned to a superseded playbook version and create\nits replacement on the approved current version -- one transaction\n(Stage E's \"un-pause the honest way\").\n\n⛔GATED ON `APPROVE`, NOT `WRITE`, and for the same reason approving a\ncampaign is: this decides who we are as a sender. A relaunch puts a\ndifferent story in front of every account the old campaign held, and\nthat is the same decision as approving the first one, arriving through a\ndifferent door.","operationId":"relaunch_campaign_api_sales_campaigns__campaign_id__relaunch_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelaunchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Relaunch Campaign Api Sales Campaigns  Campaign Id  Relaunch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/cancel":{"post":{"tags":["sales"],"summary":"Cancel Campaign","operationId":"cancel_campaign_api_sales_campaigns__campaign_id__cancel_post","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReasonRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel Campaign Api Sales Campaigns  Campaign Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/campaigns/{campaign_id}/variant-verdict":{"get":{"tags":["sales"],"summary":"Variant Verdict For Campaign","description":"Has this campaign's copy test separated its arms, and may we act on it\n(Stage E: \"reply-rate -> copy variant ... inconclusive labelled\ninconclusive\").\n\n⛔`winner` IS `null` FOR EVERY INCONCLUSIVE OUTCOME AND `leader` IS NOT A\nSUBSTITUTE FOR IT. Both are returned because a reader needs to know which\narm is ahead AND that being ahead is not yet a result; a screen that\nrenders `leader` without `outcome` beside it has reintroduced the bug\nthis endpoint exists to prevent.","operationId":"variant_verdict_for_campaign_api_sales_campaigns__campaign_id__variant_verdict_get","parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Variant Verdict For Campaign Api Sales Campaigns  Campaign Id  Variant Verdict Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/cohorts/new-agencies":{"get":{"tags":["sales"],"summary":"Get New Agencies By Month","description":"Deck measure 1: new agencies opened, one row per calendar month that\nsaw at least one -- app.modules.sales.cohort.new_agencies_by_month.","operationId":"get_new_agencies_by_month_api_sales_cohorts_new_agencies_get","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Window start, inclusive.","title":"Start"},"description":"Window start, inclusive."},{"name":"end","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Window end, exclusive.","title":"End"},"description":"Window end, exclusive."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get New Agencies By Month Api Sales Cohorts New Agencies Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/cohorts/conversion":{"get":{"tags":["sales"],"summary":"Get Cohort Conversion","description":"Every cohort year on record and how many of its agencies have since\nwon a deal -- app.modules.sales.cohort.cohort_conversion.","operationId":"get_cohort_conversion_api_sales_cohorts_conversion_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Cohort Conversion Api Sales Cohorts Conversion Get"}}}}}}},"/api/sales/cohorts/genuinely-new-share":{"get":{"tags":["sales"],"summary":"Get Genuinely New Share","description":"Deck measure 2: genuinely-new outbound as a share of all outbound in\nthe window -- app.modules.sales.cohort.genuinely_new_share.","operationId":"get_genuinely_new_share_api_sales_cohorts_genuinely_new_share_get","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Window start, inclusive.","title":"Start"},"description":"Window start, inclusive."},{"name":"end","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Window end, exclusive.","title":"End"},"description":"Window end, exclusive."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Genuinely New Share Api Sales Cohorts Genuinely New Share Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/cohorts/reply-rate/word-count":{"get":{"tags":["sales"],"summary":"Get Reply Rate By Word Count","description":"Deck measure 3 (message features): reply rate by how long a cold\nopen ran, in the sender's own words. Unwindowed by default -- see\napp.modules.sales.cohort.reply_rate_by_word_count.","operationId":"get_reply_rate_by_word_count_api_sales_cohorts_reply_rate_word_count_get","parameters":[{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Reply Rate By Word Count Api Sales Cohorts Reply Rate Word Count Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/cohorts/reply-rate/subject-word-count":{"get":{"tags":["sales"],"summary":"Get Reply Rate By Subject Word Count","description":"Deck measure 3 (message features): reply rate by subject length --\napp.modules.sales.cohort.reply_rate_by_subject_word_count.","operationId":"get_reply_rate_by_subject_word_count_api_sales_cohorts_reply_rate_subject_word_count_get","parameters":[{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Reply Rate By Subject Word Count Api Sales Cohorts Reply Rate Subject Word Count Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/cohorts/reply-rate/send-shape":{"get":{"tags":["sales"],"summary":"Get Reply Rate By Send Shape","description":"Deck measure 3 (message features): blast vs. handwritten reply rate\n-- app.modules.sales.cohort.reply_rate_by_send_shape.","operationId":"get_reply_rate_by_send_shape_api_sales_cohorts_reply_rate_send_shape_get","parameters":[{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Reply Rate By Send Shape Api Sales Cohorts Reply Rate Send Shape Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/cohorts/bid-outcomes":{"get":{"tags":["sales"],"summary":"Get Bid Outcomes","description":"Deck measure 4: how every bid ended and why --\napp.modules.sales.cohort.bid_outcomes.","operationId":"get_bid_outcomes_api_sales_cohorts_bid_outcomes_get","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Window start, inclusive (by close_date).","title":"Start"},"description":"Window start, inclusive (by close_date)."},{"name":"end","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Window end, exclusive (by close_date).","title":"End"},"description":"Window end, exclusive (by close_date)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Bid Outcomes Api Sales Cohorts Bid Outcomes Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/me/linkedin":{"get":{"tags":["sales"],"summary":"Read My Linkedin","description":"Not connected / pending / connected as <profile> -- the profile\npage's own card (`ProfilePage.tsx`, `LinkedInAccountCard`), shown only\nto a principal holding `sales:linkedin:connect`.","operationId":"read_my_linkedin_api_me_linkedin_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Read My Linkedin Api Me Linkedin Get"}}}}}}},"/api/me/linkedin/connect":{"post":{"tags":["sales"],"summary":"Connect My Linkedin","description":"\"Connect LinkedIn\". Opens a hosted-auth session\n(developer.unipile.com/reference/hostedcontroller_requestlink) the\nperson signs into LinkedIn through; the page opens `url` in a new tab.\n\nKEYLESS RETURNS `configured: false`, NEVER A DEAD LINK. No Unipile\ncredential is on file yet (CLAUDE.md \"Integrations\": key present =\nlive, key absent = stub) -- `StubUnipile.create_hosted_auth_link`\nanswers with an empty url, and this route turns that into an honest\n`configured: false` the card reads as \"not configured yet, ask for a\nUnipile credential\" rather than opening a blank tab.","operationId":"connect_my_linkedin_api_me_linkedin_connect_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Connect My Linkedin Api Me Linkedin Connect Post"}}}}}}},"/api/me/linkedin/disconnect":{"post":{"tags":["sales"],"summary":"Disconnect My Linkedin","operationId":"disconnect_my_linkedin_api_me_linkedin_disconnect_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Disconnect My Linkedin Api Me Linkedin Disconnect Post"}}}}}}},"/api/sales/linkedin/hosted-auth":{"post":{"tags":["sales"],"summary":"Hosted Auth","description":"Unipile's notify_url callback -- fires when a hosted-auth session\nfinishes (developer.unipile.com/reference/hostedcontroller_requestlink's\n`notify_url` field).\n\n⛔BEST EFFORT, ON PURPOSE. Unipile's own docs (fetched 2026-09-04) name\nCREATION_SUCCESS as notifiable this way but do not publish the payload\nshape (\"not with same format\" as the webhook events, and no example\ngiven). This reads the field names the SDK examples and the general\nwebhook envelope both use elsewhere (`account_id`, `name` -- which\n`connect_my_linkedin` set to the principal's own email) defensively,\nand never raises on an unrecognised shape: `read_my_linkedin`'s own\n`list_accounts()` reconciliation is the belt for exactly this gap.","operationId":"hosted_auth_api_sales_linkedin_hosted_auth_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Hosted Auth Api Sales Linkedin Hosted Auth Post"}}}}}}},"/api/sales/linkedin/webhook":{"post":{"tags":["sales"],"summary":"Webhook","description":"Unipile's message and relation events.\n\nNew message (developer.unipile.com/docs/new-messages-webhook):\n  {\"event\": \"message_received\", \"account_id\": ..., \"account_type\":\n   \"LINKEDIN\", \"message_id\": ..., \"message\": ..., \"timestamp\": ...,\n   \"sender\": {\"attendee_provider_id\": ..., \"attendee_profile_url\": ...,\n   \"attendee_name\": ...}}\n\nNew relation / invitation accepted\n(developer.unipile.com/docs/detecting-accepted-invitations, the\n`\"new_relation\"` webhook event -- NOT real-time, LinkedIn does not\nsupport that; Unipile polls at random intervals, up to 8h delay):\n  {\"event\": \"new_relation\", \"account_type\": \"LINKEDIN\",\n   \"user_full_name\": ..., \"user_provider_id\": ...,\n   \"user_public_identifier\": ...}","operationId":"webhook_api_sales_linkedin_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Webhook Api Sales Linkedin Webhook Post"}}}}}}},"/api/sales/linkedin/discover":{"post":{"tags":["sales"],"summary":"Discover People","description":"Search LinkedIn for people at `title_query` near `geography`, through\nthe CALLING principal's own connected LinkedIn account, and write what\nis found as contacts at `org_id` (`linkedin_discovery.search_people` /\n`upsert_found_people`).\n\nFrederick, 2026-09-04: \"I'm not convinced we have enough contact\ninformation in our system already ... maybe we do need LinkedIn to\nreach the broader corrections community\" -- only 867 of 2,328 county\nagencies on the industry map have an emailable contact.\n\n422 when the caller has no connected LinkedIn account of their own to\nsearch through -- the same \"connect one from the profile page first\"\nanswer `linkedin_sender.send_linkedin_touch`'s own gate 5 gives a send.\nKeyless-safe: no Unipile credential on file answers with an empty\n`found` list (`StubUnipile.search_people`), never a crash.","operationId":"discover_people_api_sales_linkedin_discover_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoverPeopleRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Discover People Api Sales Linkedin Discover Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/zoominfo/candidates":{"post":{"tags":["sales"],"summary":"Zoominfo Candidates","description":"Who ZoomInfo can name at this agency. COSTS NOTHING.\n\nReturns each candidate's real job title so the caller can see what it would be\npaying for. People with no email on file, and anyone in a clinical role, are\nalready dropped -- Frederick, 2026-09-08: \"not waste it on random government\npeople or the facility nurse.\"\n\nKeyless-safe: no ZoomInfo credential on file answers with an empty list\n(`StubZoomInfo`), never a crash.","operationId":"zoominfo_candidates_api_sales_zoominfo_candidates_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidatesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Zoominfo Candidates Api Sales Zoominfo Candidates Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sales/zoominfo/enrich":{"post":{"tags":["sales"],"summary":"Zoominfo Enrich","description":"Retrieve business emails for chosen candidates and write what checks out.\n\nTHIS IS THE STEP THAT SPENDS. Each person is re-searched first so the\nenrichment can be checked against a searched title -- an enrichment with\nnothing to check against is refused rather than trusted, because the vendor's\nown `FULL_MATCH` and success count are not evidence that the person is ours.\n\nAn accepted enrichment never overwrites an existing email or a human-set\nfield. Every attempt, accepted or refused, lands in `contact_enrichments`.","operationId":"zoominfo_enrich_api_sales_zoominfo_enrich_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Zoominfo Enrich Api Sales Zoominfo Enrich Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/am-capture/ingest":{"post":{"tags":["am-capture"],"summary":"Ingest","description":"Manual/test trigger for one mailbox's ingest batch. Builds the\nlive/stub Gmail adapter from settings on every call (no scheduler, no\nadapter cached across requests) -- see\napp/modules/am_capture/gmail_adapter.py build_gmail_adapter.","operationId":"ingest_api_am_capture_ingest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ingest Api Am Capture Ingest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/contacts/{contact_id}/activity":{"get":{"tags":["am-capture"],"summary":"Get Contact Activity","operationId":"get_contact_activity_api_orgs_contacts__contact_id__activity_get","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Contact Activity Api Orgs Contacts  Contact Id  Activity Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/orgs/organizations/{org_id}/activity":{"get":{"tags":["am-capture"],"summary":"Get Organization Activity","operationId":"get_organization_activity_api_orgs_organizations__org_id__activity_get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Organization Activity Api Orgs Organizations  Org Id  Activity Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/queue":{"get":{"tags":["transcription"],"summary":"List Queue","operationId":"list_queue_api_transcription_queue_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Queue Api Transcription Queue Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/next":{"get":{"tags":["transcription"],"summary":"Get Next","operationId":"get_next_api_transcription_next_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Response Get Next Api Transcription Next Get"}}}}}}},"/api/transcription/addresses/validate":{"post":{"tags":["transcription"],"summary":"Validate Address","description":"Standardise an address against USPS, and say whether it is deliverable.\n\nFrederick, 2026-08-19: \"Let's do the USPS.\"\n\nA READ-GATED WRITE-SHAPED CALL. It is a POST because an address does not\nbelong in a URL -- a street address in a query string ends up in access\nlogs and browser history -- but it changes nothing, so it takes the read\ngrant.\n\nUNCHECKED IS AN ANSWER. Without a USPS key this reports that nobody\nchecked rather than failing the request; a transcriber can still key what\nthe envelope says, which is the whole fallback the stub posture exists\nfor.\n\nISS-1912: USPS is asked for a token and then the address (30 s timeout each)\nafter two secret reads, so the route holds no pool connection: each read\ntakes a slot and gives it back before the call goes out.","operationId":"validate_address_api_transcription_addresses_validate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressToCheck"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Validate Address Api Transcription Addresses Validate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/parcel-gap":{"get":{"tags":["transcription"],"summary":"Get Parcel Gap","description":"Parcel codes we know about that Roost cannot show, both ways round.\n\nFrederick, 2026-09-18 (#1750): \"there should never be a parcel code that's\nnot in our system\", and \"we have to be able to search and find if there's\nany items that haven't been printed that should have been printed.\"\n\n`parcel_code` narrows the first list to one code, which is how somebody\nholding a label asks about the thing in their hand.\n\n2026-10-09 (Frederick: \"yes\"): a code with no parcel row that the mail\nservice printed is left out of `no_parcel_row` and `with_no_parcel_row`,\nand counted in `summary.printed_by_the_mail_service_without_a_record`. A\nsearch for that code still finds it, with the reason\n`printed_by_the_mail_service`. See `parcel_gap`'s module docstring.\n\n⛔READ `summary.never_asked_about` AND `summary.could_not_be_asked_about`\nBEFORE READING AN EMPTY `never_printed` AS GOOD NEWS. Both are pieces we\nhave no answer for, and on staging -- which holds production's\nphysical-mail pair and 403s on every vendor read -- the second is every\npiece there is. See `parcel_gap`'s own module docstring.\n\nHad only test callers from 2026-09-18 until issue #2323 (R-212): a real\nscreen (`apps/web/src/pages/fulfillment/ParcelGapCard.tsx`, on Fulfillment\n> Print batches) now calls this route.","operationId":"get_parcel_gap_api_transcription_parcel_gap_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"parcel_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Parcel Gap Api Transcription Parcel Gap Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/stats":{"get":{"tags":["transcription"],"summary":"Get Stats","description":"What actually happened today, for the empty state.\n\nTHE SCREEN USED TO SAY \"You resolved 84 today at 99.1 percent\" AND BOTH\nNUMBERS WERE HARDCODED, straight out of the design mock. Frederick read it\nafter resolving three and said so. That is the same defect class as the\ndrawn envelope: a real-looking figure that is simply made up, and the more\nplausible it looks the longer it survives.\n\nThe accuracy figure is gone rather than corrected. Accuracy needs ground\ntruth -- what the envelope really said -- and nothing here knows that; the\nonly honest measure would come from downstream corrections, which do not\nexist yet. A number nobody can compute should not be on a screen.","operationId":"get_stats_api_transcription_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Stats Api Transcription Stats Get"}}}}}}},"/api/transcription/facilities/{code}":{"get":{"tags":["transcription"],"summary":"Resolve Facility Code","description":"A facility, by the code written on the envelope.\n\nFrederick, 2026-08-19: \"the facility code is all you have to type. You\ndon't have to type the full facility address.\"\n\nTHE CODE IS THE WHOLE INPUT. A transcriber types four digits and the\nfacility, its agency and its address come back -- there is nothing else\nabout a facility worth keying by hand, and every character typed is a\ncharacter that can be mistyped. 49 facilities carry a code, and those 49\nare what Pigeonly actually ships to.","operationId":"resolve_facility_code_api_transcription_facilities__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resolve Facility Code Api Transcription Facilities  Code  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/scan":{"get":{"tags":["transcription"],"summary":"Get Item Scan","description":"The envelope itself, as a PNG.\n\nWITHOUT THIS THE STATION CANNOT BE USED. The page showed extracted fields\nand no envelope, so a transcriber had nothing to check them against and a\ncorrect read was indistinguishable from an invented one. Reading mail\nmeans looking at the mail.\n\nPROXIED, NEVER LINKED. The scan lives behind a Google Storage signed URL\nthat expires in about fifteen minutes, so a URL handed to the browser is\na dead link by the time anybody clicks it, and storing one would leak a\ncredential-bearing address into a page. Roost fetches it per request\ninstead, which also keeps the scan behind Roost's own auth rather than\nbehind a secret in a query string.\n\nONLY THE ENVELOPE. render_envelope_front returns page 1, the outside\nface, and never the letter, so there is no path from this endpoint to somebody's\nprivate correspondence.","operationId":"get_item_scan_api_transcription_items__item_id__scan_get","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/for-retrieval/{page}":{"get":{"tags":["transcription"],"summary":"Get Item Page For Retrieval","description":"Any page of a scan, for somebody about to pull that envelope by hand.\n\nFrederick, 2026-09-02: \"For transcribing an envelope, we only show the\nenvelope, not the contents. For doing a parcel return, we should show the\nfull contents because it makes it easier for the technician to identify\nthe parcel and contents. They can match the envelope, match the\nhandwriting, match the color of the paper. All that information is data\nfor them to find a parcel in a box full of paper of other parcels.\"\n\n⛔THE PIECE MUST BE ONE WE ARE ACTUALLY RETURNING, AND THAT IS CHECKED\nHERE RATHER THAN ASSUMED. Without this guard the route is \"read anyone's\ncorrespondence\" wearing a retrieval route's clothes -- the exact shape\nthis build refuses elsewhere. An OPEN return or locate card against this\nitem is the proof, so the permission expires when the job is done rather\nthan lasting forever.\n\n⛔AND IT IS A DIFFERENT ROUTE FROM `/scan`, NOT A FLAG ON IT.\n`render_envelope_sides` still exists, still returns two faces, and is\nstill what every model sees. Nothing a reader can reach changed.","operationId":"get_item_page_for_retrieval_api_transcription_items__item_id__for_retrieval__page__get","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}},{"name":"page","in":"path","required":true,"schema":{"type":"integer","title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/recent":{"get":{"tags":["transcription"],"summary":"Get Recent","operationId":"get_recent_api_transcription_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Recent Api Transcription Recent Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}":{"get":{"tags":["transcription"],"summary":"Get Item","operationId":"get_item_api_transcription_items__item_id__get","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Item Api Transcription Items  Item Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/disposition":{"post":{"tags":["transcription"],"summary":"Resolve Disposition","operationId":"resolve_disposition_api_transcription_items__item_id__disposition_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__transcription__routes__DispositionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resolve Disposition Api Transcription Items  Item Id  Disposition Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/undo":{"post":{"tags":["transcription"],"summary":"Undo Disposition","operationId":"undo_disposition_api_transcription_items__item_id__undo_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Undo Disposition Api Transcription Items  Item Id  Undo Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/skip":{"post":{"tags":["transcription"],"summary":"Skip Stale Parcel","description":"The station's answer to `NotAtReviewStage` freezing the screen\n(2026-09-25, item e58347f2). Not a disposition -- see service.\nskip_stale_parcel's own docstring; it only drops this principal's claim\nso they can move on.","operationId":"skip_stale_parcel_api_transcription_items__item_id__skip_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Skip Stale Parcel Api Transcription Items  Item Id  Skip Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/unpark":{"post":{"tags":["transcription"],"summary":"Unpark Item","description":"The manual release beside `service.release_parked_items_whose_\nparcel_is_back_at_review`'s automatic one (2026-09-25, \"a parked\nenvelope must have a RELEASE\"). SAME grant as `/skip` -- the population\ntrusted to park a row is trusted to reverse it; see service.\nunpark_item's own docstring.","operationId":"unpark_item_api_transcription_items__item_id__unpark_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unpark Item Api Transcription Items  Item Id  Unpark Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/parked":{"get":{"tags":["transcription"],"summary":"List Parked","description":"Company OS 2026-09-30 handoff, item 5 -- the \"Set aside\" tab's own\ntable (`Transcription Queues.dc.html`). Every item `/items/{id}/skip`\nparked and `/items/{id}/unpark` has not yet released, oldest first.","operationId":"list_parked_api_transcription_parked_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Parked Api Transcription Parked Get"}}}}}}},"/api/transcription/items/{item_id}/recall":{"post":{"tags":["transcription"],"summary":"Recall","operationId":"recall_api_transcription_items__item_id__recall_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Recall Api Transcription Items  Item Id  Recall Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/transcription/items/{item_id}/mark-print-batch":{"post":{"tags":["transcription"],"summary":"Mark Placed In Print Batch","description":"The seam the not-yet-built print/batching module will call once it\nlands (see service.py docstring) — exposed as a route now, same posture\nas fulfillment's Iowa-count seam (routes.py count_parcels_by_org_month),\nso it is independently testable at the HTTP layer before that module\nexists.","operationId":"mark_placed_in_print_batch_api_transcription_items__item_id__mark_print_batch_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Placed In Print Batch Api Transcription Items  Item Id  Mark Print Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/printer-routes":{"get":{"tags":["print"],"summary":"List Printer Routes","operationId":"list_printer_routes_api_print_printer_routes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Printer Routes Api Print Printer Routes Get"}}}}}}},"/api/print/batches/compose":{"post":{"tags":["print"],"summary":"Compose Batches","operationId":"compose_batches_api_print_batches_compose_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComposeBatchesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response Compose Batches Api Print Batches Compose Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/batches":{"get":{"tags":["print"],"summary":"List Batches","operationId":"list_batches_api_print_batches_get","parameters":[{"name":"facility_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","default":"recent","title":"Order"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Batches Api Print Batches Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/batches/{batch_id}":{"get":{"tags":["print"],"summary":"Get Batch","operationId":"get_batch_api_print_batches__batch_id__get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Batch Api Print Batches  Batch Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/batches/{batch_id}/dispatch":{"post":{"tags":["print"],"summary":"Dispatch Batch","operationId":"dispatch_batch_api_print_batches__batch_id__dispatch_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Dispatch Batch Api Print Batches  Batch Id  Dispatch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/batches/{batch_id}/mark-printed":{"post":{"tags":["print"],"summary":"Mark Batch Printed","operationId":"mark_batch_printed_api_print_batches__batch_id__mark_printed_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Batch Printed Api Print Batches  Batch Id  Mark Printed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/batches/{batch_id}/mark-failed":{"post":{"tags":["print"],"summary":"Mark Batch Failed","operationId":"mark_batch_failed_api_print_batches__batch_id__mark_failed_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkBatchFailedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Batch Failed Api Print Batches  Batch Id  Mark Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/envelope-scan":{"post":{"tags":["print"],"summary":"Envelope Scan By Code","description":"The scan that actually reads the barcode.\n\nResolves the code against the order Marc pushed and prints THAT envelope,\naddressed from the recipient block that arrived with it.\n\n⛔AN ORDER IN SHADOW STILL PRINTS, and this paragraph used to explain that\nwith a safeguard that does not exist: \"carrying a PARALLEL RUN - DO NOT\nMAIL band and no postage\". There is no band anywhere in `envelope.py` -- an\nearlier version drew one and it was removed as invented rather than asked\nfor -- and postage is decided by mail class, never by shadow. So a shadow\nscan produces an ordinary franked envelope, and the only thing keeping it\nout of the mail is the floor's own convention that test prints are not\nshipped. Frederick, 2026-09-08: \"We'll test the printing, but we won't test\nthe shipping ... we just won't ship those.\"\n\nThat was decided for the PARALLEL RUN, where Centric held the same order\nand a Roost print was a test print. Marc's cutover splits by routing\ninstead, so whether an unmarked duplicate should still come off the press\nis a question for him, not an assumption for this docstring.","operationId":"envelope_scan_by_code_api_print_envelope_scan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvelopeScanRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Envelope Scan By Code Api Print Envelope Scan Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/packaging-postage":{"get":{"tags":["print"],"summary":"Get Packaging Postage","description":"What the Packaging page shows about postage: how many envelopes the balance\ncovers and, when it runs short, the plain sentence (FLR-109, Frederick,\n2026-10-12: \"when it gets to the packaging step, it needs to only do enough\nenvelopes based off of postage\"). Read only: it asks for nothing and buys\nnothing. `balance_usd` and `covers` are null when the balance cannot be read,\nand then nothing about postage is claimed. Bulk mail for contracted facilities\nis never counted and never blocked.","operationId":"get_packaging_postage_api_print_packaging_postage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Packaging Postage Api Print Packaging Postage Get"}}}}}}},"/api/print/machine-health":{"get":{"tags":["print"],"summary":"Get Machine Health","operationId":"get_machine_health_api_print_machine_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response Get Machine Health Api Print Machine Health Get"}}}}}}},"/api/print/ready":{"get":{"tags":["print"],"summary":"Get Ready To Print","description":"The ready-to-print list under a filter: the run it would make, in its two\nsections (contract facility mail, everything else), and the contract\nfacilities a person may put first (`first`, in the order added; it orders\nthe run and never narrows it, FLR-104). The report half of\n`POST /api/print/runs`: both read `mfg_ingest._UNPRINTED_PREDICATE` through\n`print_runs`, so the number on the button is the number sent.","operationId":"get_ready_to_print_api_print_ready_get","parameters":[{"name":"first","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"default":[],"title":"First"}},{"name":"product","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"default":[],"title":"Product"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Source"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"To"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Ready To Print Api Print Ready Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/feed":{"get":{"tags":["print"],"summary":"Get The Print Feed","description":"What Rooster would print next, and why (FLR-109, issue #4503, plan slice 2;\nREAD-ONLY, nothing is sent, whatever the setting `print_rooster_feed` says).\n\n`next` is the whole line in order, the head first: the contracted facility\nfurthest behind (earliest SLA deadline, then oldest piece, to the\nmillisecond), new mail for the facility printing joining it when the\ndeadline check allows, and ALL OTHER MAIL last, oldest first, with no\ndeadline (it has no SLA). `figures` is the Printing page's ready, printed\ntoday, oldest and finish. `printers` says which printers are running and\nready; nothing is planned onto one that is not (Frederick, 2026-10-12).","operationId":"get_the_print_feed_api_print_feed_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get The Print Feed Api Print Feed Get"}}}}}}},"/api/print/feed/start":{"post":{"tags":["print"],"summary":"Start Printing","description":"Start printing: Rooster feeds the presses until somebody pauses or nothing is\nleft (FLR-109, issue #4503; Frederick, 2026-10-12: \"the human really should only\nhit a button that says like start printing\"). Writes one switch row with who\npressed it and when, and resumes the batches a Pause paused. Refused (409) while\nthe setting `print_rooster_feed` is off.","operationId":"start_printing_api_print_feed_start_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PressTheSwitchRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Start Printing Api Print Feed Start Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/feed/pause":{"post":{"tags":["print"],"summary":"Pause Printing","description":"Pause printing: Rooster sends nothing more and pauses its waiting and printing\nbatches; the jobs already handed to a printer finish (FLR-109, plan 4.7).\nAlways allowed. Writes one switch row with who pressed it and when.","operationId":"pause_printing_api_print_feed_pause_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PressTheSwitchRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pause Printing Api Print Feed Pause Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/feed/steers":{"post":{"tags":["print"],"summary":"Steer Printing","description":"A manager tells Rooster how to steer today's printing line, in plain words\n(\"give Iowa first today\", \"move Fresno to third\", \"hold Tulare today\"). Frederick,\n2026-10-12 (issue #4503, FLR-109 item 7): \"a manager should be able to hold a\nfacility, change the facility order, bump somebody to the front of the list.\"\nThe manager is the principal on the row. The deadline check is the one\nguardrail: a bump or move that would make another facility miss is recorded as\nrefused (`accepted: false`, `refusal` in plain English) and the order stands.\n422 `steer_not_understood` when the words or the facility cannot be read.\n`feed.steers[]` is the receipt. Writes order only; nothing is sent.","operationId":"steer_printing_api_print_feed_steers_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SteerRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Steer Printing Api Print Feed Steers Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/feed/days":{"get":{"tags":["print"],"summary":"Get The Print Feed Days","description":"Is printing getting better or worse? (FLR-109 item 8, issue #4503, plan slice 11.)\nOne row per floor day for the last `weeks` weeks (contracted facilities only: on-time\nshare, age at ship, the Packaging idle minutes, the jumps) and the baseline of the four\nweeks before Rooster printed, so a person sees both on one screen. READ-ONLY.","operationId":"get_the_print_feed_days_api_print_feed_days_get","parameters":[{"name":"weeks","in":"query","required":false,"schema":{"type":"integer","maximum":52,"minimum":1,"default":4,"title":"Weeks"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get The Print Feed Days Api Print Feed Days Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/runs":{"post":{"tags":["print"],"summary":"Send A Print Run","description":"Send a print run: queue every order the filter picks, in the press's\norder (contract facilities one at a time, then everything else oldest to\nnewest; `first` bumps facilities to the front), as one act (FLR-91, FLR-104). 409 with a `code` and the sentence the screen\nsays when nothing was sent: `nothing_to_send`, `run_changed`,\n`printer_not_answering`, `order_cannot_print` (every order the run picked\nwas held, or one failed for a reason that is not a hold); 422 for a\nmalformed filter. An order that cannot print is HELD, left out, and said\nin `held` / `held_message` while the rest is sent (PGS-2).","operationId":"send_a_print_run_api_print_runs_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRunRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send A Print Run Api Print Runs Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["print"],"summary":"List Print Runs","description":"The last runs: number, size, what was asked, who sent it, and when its\nlast page printed (None while any of it is still to print).","operationId":"list_print_runs_api_print_runs_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Print Runs Api Print Runs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/runs/{run_id}/put-back":{"post":{"tags":["print"],"summary":"Put A Test Runs Orders Back","description":"A person presses \"Put these orders back now\" on a finished TEST run\n(FLR-105, Frederick 2026-10-09: \"automatically reset that order so it can be\nprinted again\"): every order of the run goes back to the ready list at once,\nwithout waiting for its envelope. The system does the same by itself when\nthe envelope has printed or a bounded wait has passed. 404: no such run;\n409 with a `code`: `not_a_test_run`, or `still_printing` (a job on a press\ncannot be taken back); nothing is changed on a refusal.","operationId":"put_a_test_runs_orders_back_api_print_runs__run_id__put_back_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put A Test Runs Orders Back Api Print Runs  Run Id  Put Back Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/queue":{"get":{"tags":["print"],"summary":"Get Print Queue","description":"The batches waiting for, and draining into, each printer, in queue order,\nwith progress and which buttons apply; and the printer errors that are\nstopping batches right now (the banner at the top of the Printing page).","operationId":"get_print_queue_api_print_queue_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Print Queue Api Print Queue Get"}}}}}}},"/api/print/queue/{run_id}/orders":{"get":{"tags":["print"],"summary":"Get Batch Orders","description":"Every order in one batch and where it stands: waiting, sent to the\nprinter, printed or failed.","operationId":"get_batch_orders_api_print_queue__run_id__orders_get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Batch Orders Api Print Queue  Run Id  Orders Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/queue/{run_id}/pause":{"post":{"tags":["print"],"summary":"Pause A Batch","description":"Stop sending a batch's jobs to the printer. Jobs already on it carry on.","operationId":"pause_a_batch_api_print_queue__run_id__pause_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pause A Batch Api Print Queue  Run Id  Pause Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/queue/{run_id}/resume":{"post":{"tags":["print"],"summary":"Resume A Batch","description":"Start a paused batch again, or carry on after a printer error somebody\nhas looked at.","operationId":"resume_a_batch_api_print_queue__run_id__resume_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resume A Batch Api Print Queue  Run Id  Resume Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/queue/{run_id}/move":{"post":{"tags":["print"],"summary":"Move A Batch","description":"Bump a waiting batch up or down its printer's queue (409 when it is not\nwaiting, or already first or last).","operationId":"move_a_batch_api_print_queue__run_id__move_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveBatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Move A Batch Api Print Queue  Run Id  Move Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/queue/{run_id}/cancel":{"post":{"tags":["print"],"summary":"Cancel A Batch","description":"Cancel a batch that has not started printing; its orders go back to the\nready list.","operationId":"cancel_a_batch_api_print_queue__run_id__cancel_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cancel A Batch Api Print Queue  Run Id  Cancel Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/queue/{run_id}/orders/{order_id}/drop":{"post":{"tags":["print"],"summary":"Drop An Order From A Batch","description":"Take one order out of a batch that has not started; it goes back to the\nready list.","operationId":"drop_an_order_from_a_batch_api_print_queue__run_id__orders__order_id__drop_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Run Id"}},{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Drop An Order From A Batch Api Print Queue  Run Id  Orders  Order Id  Drop Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/orders/{order_id}/reprint":{"post":{"tags":["print"],"summary":"Reprint An Order","description":"Print an order again, with a reason: a new batch at the back of the\nqueue. The only way anything prints twice. 422 without a reason; 409 when\nnothing of it has printed or a piece is still on its way.","operationId":"reprint_an_order_api_print_orders__order_id__reprint_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReprintOrderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reprint An Order Api Print Orders  Order Id  Reprint Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/jobs/{job_id}/settle":{"post":{"tags":["print"],"summary":"Settle Failed Job","description":"Settle one `failed` print job as `printed`, `on_the_press` or\n`withdrawn`, with a reason. 404 no such job; 409 it is not failed (any\nmore); 422 an unknown outcome, no reason, or `printed` with no evidence.","operationId":"settle_failed_job_api_print_jobs__job_id__settle_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettleJobRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Settle Failed Job Api Print Jobs  Job Id  Settle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/printer-routes/{route_id}/restart-after-error":{"post":{"tags":["print"],"summary":"Restart After Error","description":"Re-send everything this press did not finish, from the last job it did.\n\nEvery job sent after the last completed one that is not completed goes back\nto the FRONT of the queue, in its original order, marked \"redo: printer\nerror <words>\". No completed job is ever re-sent. It also CLEARS the stop on\nevery batch this printer's error paused (a person's Resume, 2026-10-13: the\nautomatic restart had already re-queued the jobs, so the page's Resume did\nnothing). 404 no such route; 409 the route is not a press, or there was\nnothing to restart (a printer that merely did not confirm is not an error)\nand no stopped batch to clear.","operationId":"restart_after_error_api_print_printer_routes__route_id__restart_after_error_post","parameters":[{"name":"route_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Route Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestartAfterErrorRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restart After Error Api Print Printer Routes  Route Id  Restart After Error Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/failed-jobs":{"get":{"tags":["print"],"summary":"List Failed Jobs","description":"One page (50 rows) of the Failed tab, newest first: order, run,\nfacility, product, what went wrong in plain words and what to do, whether the\npage may have printed (`outcome_unknown`), who started it, when, and whether\nits line is still owed (`actionable`).\n\n`older=true` adds the failures no longer owed (\"Show older\"). The jobs the\nprinter never confirmed are NOT rows here: they come back as `unconfirmed`,\n`{count, runs: [{run_number, jobs, since, started_by}]}`, one entry per run,\nwith the count of ALL of them (S1 of the volume audit, 2026-10-08: a run\nflagged wholesale is one line, not a thousand). `unconfirmed_run=<run\nnumber>` (0 for jobs with no run) swaps the rows for that run's unconfirmed\njobs, paged the same way. `owed`, `older_count` and `unconfirmed.count` are\nover every page; `total` is the rows in the view being paged.","operationId":"list_failed_jobs_api_print_failed_jobs_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Page Size"}},{"name":"older","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Older"}},{"name":"unconfirmed_run","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Unconfirmed Run"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Failed Jobs Api Print Failed Jobs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/failed-jobs/runs/{run_number}/settle":{"post":{"tags":["print"],"summary":"Settle Run","description":"Settle the jobs of one run the printer never confirmed, as `printed` or\n`on_the_press`, up to 200 per call (`failed_jobs.settle_run`; `run_number` 0\nis jobs with no run). Each job goes through the same act as its own button,\nso each keeps its own history. Answers `settled`, `skipped` (left for a\nperson to settle one by one), `remaining` and `next_after`: send that back\nas `after` until it is null. 422 for an outcome that is not one of the two.\nNothing is sent to a printer.","operationId":"settle_run_api_print_failed_jobs_runs__run_number__settle_post","parameters":[{"name":"run_number","in":"path","required":true,"schema":{"type":"integer","title":"Run Number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettleRunRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Settle Run Api Print Failed Jobs Runs  Run Number  Settle Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/failed-jobs/{job_id}/send-again":{"post":{"tags":["print"],"summary":"Send Failed Job Again","description":"The Send again button: the order goes back in the ready list for the next run a\nperson composes. Nothing is queued or sent to a printer. 404 no such\nfailed-tab job; 409 it is not failed (any more).","operationId":"send_failed_job_again_api_print_failed_jobs__job_id__send_again_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Send Failed Job Again Api Print Failed Jobs  Job Id  Send Again Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/failed-jobs/{job_id}/mark-printed":{"post":{"tags":["print"],"summary":"Mark Failed Job Printed","description":"The It printed button: the piece came out, so the job is marked printed and\nnothing reprints. 404 no such failed-tab job; 409 it is not failed (any\nmore); 422 the job does not say which line of a several-line order it\nprinted (Send again still works).","operationId":"mark_failed_job_printed_api_print_failed_jobs__job_id__mark_printed_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Failed Job Printed Api Print Failed Jobs  Job Id  Mark Printed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/duplicates":{"get":{"tags":["print"],"summary":"List Possible Duplicates","operationId":"list_possible_duplicates_api_print_duplicates_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Possible Duplicates Api Print Duplicates Get"}}}}}}},"/api/print/duplicates/{flag_id}/verdict":{"post":{"tags":["print"],"summary":"Decide Possible Duplicate","operationId":"decide_possible_duplicate_api_print_duplicates__flag_id__verdict_post","parameters":[{"name":"flag_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Flag Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateVerdictRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decide Possible Duplicate Api Print Duplicates  Flag Id  Verdict Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/reprints":{"get":{"tags":["print"],"summary":"List Reprints","operationId":"list_reprints_api_print_reprints_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Reprints Api Print Reprints Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/reprints/{action_item_id}":{"get":{"tags":["print"],"summary":"Get Reprint","operationId":"get_reprint_api_print_reprints__action_item_id__get","parameters":[{"name":"action_item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Action Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Reprint Api Print Reprints  Action Item Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/reprints/{action_item_id}/approve":{"post":{"tags":["print"],"summary":"Approve Reprint","operationId":"approve_reprint_api_print_reprints__action_item_id__approve_post","parameters":[{"name":"action_item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Action Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Reprint Api Print Reprints  Action Item Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/reprints/{action_item_id}/decline":{"post":{"tags":["print"],"summary":"Decline Reprint","operationId":"decline_reprint_api_print_reprints__action_item_id__decline_post","parameters":[{"name":"action_item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Action Item Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeclineReprintRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decline Reprint Api Print Reprints  Action Item Id  Decline Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/print/tracking/{tracking_number}":{"get":{"tags":["print"],"summary":"Get Tracking Status","description":"The current status Roost holds for one tracking number.\n\nENSURES THE ROW EXISTS rather than 404ing: a number closed seconds ago,\nbefore tracking_status_worker's own discovery step next runs, must read\nas \"not checked yet\" (status \"unknown\", every timestamp null) rather\nthan as an error -- and creating the row here is what makes the very\nnext worker cycle pick it up first, per\ntracking_status.ensure_tracked's own docstring.","operationId":"get_tracking_status_api_print_tracking__tracking_number__get","parameters":[{"name":"tracking_number","in":"path","required":true,"schema":{"type":"string","title":"Tracking Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Tracking Status Api Print Tracking  Tracking Number  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/today/role":{"get":{"tags":["today"],"summary":"Get Role Config","description":"Today's blocks for the signed-in person, and which role they are.\n\n⛔THE FOUR TILES ARE NOT IN HERE. `GET /api/metrics/tiles` already answers\nwhat a role is held to, for all eight roles, and this returns `score: []`\nso the caller fills it from there. Two services computing one scorecard is\nthe spaghetti this build has a CI gate about.\n\n⛔ALWAYS THEIR OWN, NEVER SOMEBODY ELSE'S. There is no principal_id\nparameter and there is not going to be one: reading another person's\nstanding used to be the mirror's question (`mirror_service.py`, coach-\ndown / report-up / never-sideways), deleted 2026-09-25 as dead Today v3\ncode with no caller -- see this module's own docstring. If a \"view\nsomeone else's standing\" feature returns, the same access question\n(twice now a grant meant to gate it turned out to be held for an\nunrelated reason) needs deciding again, not copied from the deleted\nversion without re-checking who actually holds which grant.","operationId":"get_role_config_api_today_role_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Role Config Api Today Role Get"}}}}}}},"/api/today/notices/{announcement_id}/ack":{"post":{"tags":["today"],"summary":"Acknowledge Notice","description":"\"I have read this\" on a Notices card (Today v4 data gap #1966).\n\n⛔THE WRITE THE BUTTON NEVER HAD. `TodayPage.tsx`'s old `onAcknowledge`\nonly set local React state (`setAcked`) -- a refresh, or a second\ndevice, forgot it ever happened. This calls the real ledger\n(`announcements.acknowledge`, migration 0530, Track E15), on the `web`\nchannel `ACK_CHANNELS` names for exactly this: acknowledging on the\nmanager screen without Roost ever having sent on that channel.\n\nAlways the CALLING principal's own acknowledgement -- no principal_id\nparameter, same discipline as `get_role_config` above. `acknowledged:\nfalse` (never a 404/403) when the caller was not this announcement's\nrecipient, or had already acknowledged it: `acknowledge`'s own\ndocstring calls that refusal \"the positive control for the whole\ntable\", and this route is not the place to turn it into an error the\ncaller has to specially handle for what is, from their side, already\ntrue either way.","operationId":"acknowledge_notice_api_today_notices__announcement_id__ack_post","parameters":[{"name":"announcement_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Announcement Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Acknowledge Notice Api Today Notices  Announcement Id  Ack Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/tiles":{"get":{"tags":["metrics"],"summary":"List Tiles","description":"The four tiles for a role, each with a value or the reason it has none.\n\n⛔A TILE WITHOUT A VALUE ALWAYS CARRIES A REASON. The response never\nsubstitutes 0 -- design brief 9: \"a blank or zero is a lie\" -- and the\ncaller is told WHICH kind of empty it is, because they are different facts:\na metric with no feed at all names the missing input (\"needs a roadmap\"),\nwhile a wired metric whose window held nothing says so instead. Rendering\nboth as 0 would tell a customer service rep they resolved nothing on a week\nwhen no tickets arrived.\n\n`definition` is returned on every tile and is never blank. It is what turns\na red tile from a judgement into a standard, and the schema refuses a\ndefinition shorter than a sentence for that reason.","operationId":"list_tiles_api_metrics_tiles_get","parameters":[{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Which of the eight Today roles. Omit it for your own, which is what Today asks for.","title":"Role"},"description":"Which of the eight Today roles. Omit it for your own, which is what Today asks for."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Tiles Api Metrics Tiles Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals":{"get":{"tags":["metrics"],"summary":"List Goals","description":"Every goal, proposed and agreed alike.\n\nAdmin shows both, because the proposed ones ARE the conversation. Only a\nscorecard filters, and it filters through `goals.agreed_goal_for`.","operationId":"list_goals_api_metrics_goals_get","parameters":[{"name":"include_retired","in":"query","required":false,"schema":{"type":"boolean","description":"Also return retired goals -- the admin trash view, so a mis-retired or purge-candidate goal can be found and restored.","default":false,"title":"Include Retired"},"description":"Also return retired goals -- the admin trash view, so a mis-retired or purge-candidate goal can be found and restored."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Goals Api Metrics Goals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["metrics"],"summary":"Propose Goal","description":"Propose a target. Nobody is held to it yet.\n\n⛔IT IS DELIBERATELY IMPOSSIBLE TO CREATE AN AGREED GOAL IN ONE STEP.\nAgreeing is a separate act by a separate person; collapsing the two would\nlet whoever proposes a target also sign it off, which is the whole thing\nthe rule exists to prevent.","operationId":"propose_goal_api_metrics_goals_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Propose Goal Api Metrics Goals Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/board":{"get":{"tags":["metrics"],"summary":"Goals Board","description":"The Goals \"In force\" tab, whole (Mandate.dc.html): every goal as a card\nwith its live reading or an honest \"cannot be measured\", the couplings that\nsay which goal's movement buys which, and the state variables the company\nasserts against a computed suggestion. Every figure is a read; a goal with\nno live feed names the missing feed instead of showing a zero.","operationId":"goals_board_api_metrics_goals_board_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Goals Board Api Metrics Goals Board Get"}}}}}}},"/api/metrics/goals/extract":{"post":{"tags":["metrics"],"summary":"Extract Goals","description":"The considered version of what Frederick said (Mandate.dc.html's\n`showPlayback` state): every distinct goal statement the transcript\nholds, each with the exact words it came from. Nothing here is in\nforce -- that is a separate act, `POST /goals/confirm`, one item at a\ntime.\n\nGated the same as every other goal write: this is where the goals come\nfrom, not a read of them.","operationId":"extract_goals_api_metrics_goals_extract_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalTranscript"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Extract Goals Api Metrics Goals Extract Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/confirm":{"post":{"tags":["metrics"],"summary":"Confirm Extracted Goal","description":"\"This is right, hold me to it\" (Mandate.dc.html L174, L587): the one\nclick that both proposes and agrees a goal extracted from what Frederick\nsaid.\n\nTwo calls under the hood, not one -- `goals.create_child_goal` then\n`goals.agree`, the same two acts Admin's own goal editor makes for any\nother goal. The separation of \"proposed\" from \"agreed\" stays real in the\naudit trail even though one click triggers both here: it is only the\nplain `POST /goals` route (`goals.propose`) that is barred from doing\nboth in a single call, because THAT route lets whoever proposes also be\nwhoever signs off with no further act at all. Here the further act\nhappens -- `goals.agree` -- it is just chained by the same already\ngoals-write-gated principal in the same request, exactly mirroring \"he\nproposes, he agrees\" done by hand on two clicks instead of one.\n\nCreated UNMEASURED (no metric_key, no target) by default: nothing in a\nfreeform sentence safely resolves to a wired feed, and a goal with no\nfeed already has an honest home on this board -- the same \"cannot be\nmeasured, here is why\" card most of the goals on the live design wear.\n\n⛔EXCEPT A GROWTH GOAL THAT NAMES A NUMBER AND A PERIOD (U2, 2026-09-25):\n\"two county meetings a month\" is created MEASURABLE, bound to\n`marketing.meetings_booked` with the stated target and window --\nsee `_parse_measurable_meetings_goal` for the rule and its scope.","operationId":"confirm_extracted_goal_api_metrics_goals_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalConfirmation"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Confirm Extracted Goal Api Metrics Goals Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/{goal_id}":{"patch":{"tags":["metrics"],"summary":"Amend Goal","description":"Change a goal, snapshotting what it said first.\n\nFrederick, 2026-09-09: \"I need to be able to update and change goals. The\ngoals can't be set forever.\" Until this route existed the goal surface was\npropose / agree / withdraw and nothing else -- no way to move a target, a\ndeadline, or a word.\n\nA change to the SUBSTANCE takes the agreement back (`goals.\nSUBSTANTIVE_FIELDS`); re-wording the narrative does not. The response says\nwhich happened, because \"your edit landed\" and \"your edit landed and this\ngoal now needs signing off again\" are different things to be told.","operationId":"amend_goal_api_metrics_goals__goal_id__patch","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalAmendment"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Amend Goal Api Metrics Goals  Goal Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/{goal_id}/retire":{"post":{"tags":["metrics"],"summary":"Retire Goal","description":"This is no longer one of the company's goals.\n\nSoft and reversible. `goals.retire`'s own docstring says why a delete\nwould be wrong: every drift verdict and decision recorded while it stood\nmust keep resolving against it. It refuses the company root, and refuses\na goal something still hangs off, naming the children rather than\nstranding them.","operationId":"retire_goal_api_metrics_goals__goal_id__retire_post","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalRetirement"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Retire Goal Api Metrics Goals  Goal Id  Retire Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/{goal_id}/restore":{"post":{"tags":["metrics"],"summary":"Restore Goal","description":"Bring a retired goal back, UNAGREED. A one-way door on this page is\nhow somebody ends up living with a mis-click.","operationId":"restore_goal_api_metrics_goals__goal_id__restore_post","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Restore Goal Api Metrics Goals  Goal Id  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/{goal_id}/agree":{"post":{"tags":["metrics"],"summary":"Agree Goal","description":"The owner agrees. From here the tile shows the target and who set it.","operationId":"agree_goal_api_metrics_goals__goal_id__agree_post","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Agree Goal Api Metrics Goals  Goal Id  Agree Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/{goal_id}/withdraw":{"post":{"tags":["metrics"],"summary":"Withdraw Goal","description":"Take the agreement back, keeping the goal.\n\nA target somebody withdrew is a different fact from one nobody ever set,\nand deleting the row would take the conversation with it.","operationId":"withdraw_goal_api_metrics_goals__goal_id__withdraw_post","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Withdraw Goal Api Metrics Goals  Goal Id  Withdraw Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/metrics/goals/{goal_id}/purge":{"get":{"tags":["metrics"],"summary":"Preview Goal Purge","description":"What purging this goal would do -- purgeable or not, and why.","operationId":"preview_goal_purge_api_metrics_goals__goal_id__purge_get","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Preview Goal Purge Api Metrics Goals  Goal Id  Purge Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["metrics"],"summary":"Purge Goal","description":"Hard delete. Refuses by name (`goals.GoalConstraintViolated`) for\nanything but an unagreed, childless, unreferenced test goal -- see\n`goals.purge_check` for the exact rule. A receipt (`goal_purges`) is\nwritten before the row goes, because after this call there is nothing\nleft to read back.","operationId":"purge_goal_api_metrics_goals__goal_id__purge_post","parameters":[{"name":"goal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Goal Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalPurge","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Purge Goal Api Metrics Goals  Goal Id  Purge Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commitments":{"post":{"tags":["commitments"],"summary":"Post Commitment","description":"Write down what the signed-in principal owes, to whom, and by when.","operationId":"post_commitment_api_commitments_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommitmentRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Commitment Api Commitments Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/commitments/{commitment_id}/keep":{"post":{"tags":["commitments"],"summary":"Post Keep Commitment","description":"Record the moment a commitment was met.","operationId":"post_keep_commitment_api_commitments__commitment_id__keep_post","parameters":[{"name":"commitment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Commitment Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeepCommitmentRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Keep Commitment Api Commitments  Commitment Id  Keep Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/blocks":{"post":{"tags":["commitments"],"summary":"Post Block","description":"Record that the signed-in principal's work cannot proceed until\nsomebody else decides.","operationId":"post_block_api_blocks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBlockRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Block Api Blocks Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/blocks/{block_id}/resolve":{"post":{"tags":["commitments"],"summary":"Post Resolve Block","description":"Record the moment the decider actually decided.","operationId":"post_resolve_block_api_blocks__block_id__resolve_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveBlockRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Resolve Block Api Blocks  Block Id  Resolve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/decisions/next":{"get":{"tags":["decisions"],"summary":"Get Next Decision","description":"The single decision waiting on the founder, oldest first, and how many\nothers are behind it -- `NextDecisionWire`.\n\n⛔GATED TO THE SINGLE OWNER-ROLE HOLDER. `decisions` (migration 0376)\ncarries no assignee column, so this is honestly one person's queue only\nwhile exactly one person holds the owner role -- see\n`service.is_the_singular_owner`'s own docstring, which is the identical\nguard `metrics.resolvers.owner_approvals_waiting_on_you` already applies\nto the COUNT of this same ledger. This route hands back full decision\ncontent, so it holds to at least that same standard rather than a\nlooser one.","operationId":"get_next_decision_api_decisions_next_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Next Decision Api Decisions Next Get"}}}}}}},"/api/decisions/ladder":{"get":{"tags":["decisions"],"summary":"Get Ladder","description":"Every decision class, its rung, and its standing -- `LadderWire`.\n\nUnlike `/next`, this is not gated to the owner: the ladder is the\ncompany's own autonomy record, not one person's plate, and every class\nsits at rung 0 in v1 regardless of who is reading it. It IS gated to the\nchairman (0398) -- see the module docstring.","operationId":"get_ladder_api_decisions_ladder_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ladder Api Decisions Ladder Get"}}}}}}},"/api/decisions/{decision_id}":{"get":{"tags":["decisions"],"summary":"Get Decision Route","description":"One decision -- the same wire shape and the same blind rules as\n`/next`. Gated to the singular owner for the identical reason `/next` is:\nthis hands back full decision content, including the situation and the\npriced options, and after a pick, the recommendation and its\nconfidence.","operationId":"get_decision_route_api_decisions__decision_id__get","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Decision Route Api Decisions  Decision Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/decisions/{decision_id}/pick":{"post":{"tags":["decisions"],"summary":"Pick Option","description":"The founder picks. THIS is the response `reveal` is populated in.\n\n⛔`presented_blind` IS NEVER TAKEN FROM THE REQUEST. It is derived here,\nserver-side, from `queue.is_presented_blind(decision_id)` -- exactly what\n`/next` and `GET /{decision_id}` used to decide whether to withhold the\nreveal in the first place -- and NEVER from anything the caller sent, the\nsame rule `_wire_decision`'s own docstring states for reading it. A caller\nthat could assert its own blindness could manufacture blind-agreement\nevidence out of a sighted click.\n\nThe chosen option is validated against the decision's own options here,\nBEFORE `service.choose` is ever called, using the same `option_id` the\nweb was handed (`_option_id`/`_option_by_id`) rather than a label a\ncaller could misspell into a false negative. `service.choose` re-checks\nthe label regardless -- belt and braces, never a reason to skip this\none -- and its own guarded UPDATE is what actually holds under a race,\nsurfaced below as 409 rather than swallowed.","operationId":"pick_option_api_decisions__decision_id__pick_post","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PickRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pick Option Api Decisions  Decision Id  Pick Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/decisions/{decision_id}/rule":{"post":{"tags":["decisions"],"summary":"Decide Rule Route","description":"A board item's answer: accept the proposed rule, edit it, or decline\nit. Persists through `service.decide_rule` to migration 0381's real\ncolumns -- see the module docstring for why `tie.proposed_rule` itself\nis still a stated \"not yet\" rather than a stored one.","operationId":"decide_rule_route_api_decisions__decision_id__rule_post","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleDecisionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decide Rule Route Api Decisions  Decision Id  Rule Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/briefing/weekly":{"get":{"tags":["briefing"],"summary":"Get Weekly","description":"The latest briefing, or a named week's own -- `?week_start=2026-09-01`.\nAn honest \"not yet\" body rather than a 404 for the common case (nothing\ngenerated yet), matching `GET /api/decisions/next`'s own shape.","operationId":"get_weekly_api_briefing_weekly_get","parameters":[{"name":"week_start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Week Start"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Weekly Api Briefing Weekly Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/briefing/offcadence":{"get":{"tags":["briefing"],"summary":"Get Offcadence Queue","description":"Every off-cadence contact still waiting on his rating, oldest first.","operationId":"get_offcadence_queue_api_briefing_offcadence_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Offcadence Queue Api Briefing Offcadence Get"}}}}}}},"/api/briefing/offcadence/score":{"get":{"tags":["briefing"],"summary":"Get Offcadence Score","description":"Precision and recall, per initiator -- `service.offcadence_score`'s\nown wire.","operationId":"get_offcadence_score_api_briefing_offcadence_score_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Offcadence Score Api Briefing Offcadence Score Get"}}}}}}},"/api/briefing/offcadence/{contact_id}/respond":{"post":{"tags":["briefing"],"summary":"Respond To Contact Route","description":"His one click. Guarded in the database, not in Python --\n`service.respond_to_offcadence_contact`'s own docstring.","operationId":"respond_to_contact_route_api_briefing_offcadence__contact_id__respond_post","parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RespondRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Respond To Contact Route Api Briefing Offcadence  Contact Id  Respond Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/briefing/late-notice":{"post":{"tags":["briefing"],"summary":"Flag Late Notice","description":"Recall's ground truth, recorded by his own hand.","operationId":"flag_late_notice_api_briefing_late_notice_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LateNoticeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Flag Late Notice Api Briefing Late Notice Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/boardroom":{"get":{"tags":["boardroom"],"summary":"Get Boardroom","description":"The page, one call: header holders and waiting-on-you (worst first).\n\nCORRECTED 2026-10-08 (old-shape cleanup, B1 of\n`OLD_SHAPE_CODE_AUDIT_20261008.md`): this used to also compute \"the\nbusiness underneath\" (`money`, `people` and `people_footer`, `growth`,\n`risk`), a `stale` banner that was always `null`, and the decision\n`record`. Frederick's S9 ruling of 2026-10-02 (decision 2) deleted those\nsections from the page (\"Money, people and growth figures now link out to\n/today/company\"; the historical record and desk-rating comparison \"deleted\nfrom this page outright\"), and the 2026-10-04 appended correction removed\nthe last link-out and the stale banner. `BoardroomPage.tsx` reads only\n`holders` and `waiting.decisions`, so the server stopped doing the work\n(and stopped ranking people, which the page's own footer said could never\nbe shown to them).","operationId":"get_boardroom_api_boardroom_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Boardroom Api Boardroom Get"}}}}}}},"/api/boardroom/decisions/{decision_id}":{"get":{"tags":["boardroom"],"summary":"Get Decision","operationId":"get_decision_api_boardroom_decisions__decision_id__get","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Decision Api Boardroom Decisions  Decision Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/boardroom/decisions/{decision_id}/commit":{"post":{"tags":["boardroom"],"summary":"Commit Decision","description":"Rule on the decision: the chosen option and the note are stamped\ntogether. (The \"ratify / override / cast the deciding vote\" three-way act\nwent with the vote mechanics, 2026-10-08, see `_decision_wire`.)","operationId":"commit_decision_api_boardroom_decisions__decision_id__commit_post","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Commit Decision Api Boardroom Decisions  Decision Id  Commit Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/boardroom/decisions/{decision_id}/hold":{"post":{"tags":["boardroom"],"summary":"Hold Decision","operationId":"hold_decision_api_boardroom_decisions__decision_id__hold_post","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Hold Decision Api Boardroom Decisions  Decision Id  Hold Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/boardroom/decisions/{decision_id}/return":{"post":{"tags":["boardroom"],"summary":"Return Decision","operationId":"return_decision_api_boardroom_decisions__decision_id__return_post","parameters":[{"name":"decision_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Decision Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Return Decision Api Boardroom Decisions  Decision Id  Return Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/boardroom/grants":{"get":{"tags":["boardroom"],"summary":"List Grants","description":"Who can open this page -- linked from the header, and from Admin's\nown grant management (`admin_routes.py`), not duplicated here.","operationId":"list_grants_api_boardroom_grants_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Grants Api Boardroom Grants Get"}}}}}},"post":{"tags":["boardroom"],"summary":"Create Grant","description":"Only a current WRITE holder may hand the keys to someone else -- \"a\nprivate page should say out loud who else is in the room\", and the room\ndecides who else joins it, at whichever level it chooses.","operationId":"create_grant_api_boardroom_grants_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Grant Api Boardroom Grants Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/decision-classes/{class_key}/remit":{"put":{"tags":["chairman"],"summary":"Set Decision Class Remit","description":"Put a decision class in the board's remit, or take it out.\n\n⛔THIS IS WHERE FREDERICK'S ANSWER LANDS, AND IT EXISTS SO THAT AN AGENT\nIS NEVER THE ONE WHO WRITES IT. The §6 question is \"which decisions do\nyou want the board to decide?\" -- and without a place for the answer, the\nday he answers it somebody writes a migration in his name.\n\n⛔IT IS A REMIT, NOT A THRESHOLD. Nothing in this schema orders decisions\nby size: `magnitude_band` is free text and `decisions.stake` is text. So\nthe board's scope is a list, class by class.\n\n⛔CORRECTED 2026-10-04 (#3592; the text this replaces said\n`decision_classes.board_reviews` \"defaults FALSE -- nothing reaches the\nboard until he says so here\", which was the 2026-09-20 rule, migration\n0511). RA-24 (2026-09-25, Frederick: \"Everything important\") superseded it:\n`decisions.service.register_class` stamps `board_reviews = true` on every\nNON-ROUTINE class at birth, attributed to the chairman, and migration 0655\nbackfilled the existing ones. Only a routine, reversible in-house class\nstarts outside the remit. So this route is how a class goes OUT of the\nremit (or a routine one goes in), and a later registration never overrides\nthe choice.\n\nThe class records who said it (`board_reviews_set_by`/`_at`, 0512), and a\nCHECK refuses a remit with nobody attributed -- the same discipline 0376\napplied to the promotion threshold.","operationId":"set_decision_class_remit_api_chairman_decision_classes__class_key__remit_put","parameters":[{"name":"class_key","in":"path","required":true,"schema":{"type":"string","title":"Class Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Decision Class Remit Api Chairman Decision Classes  Class Key  Remit Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/board-seats/{seat_key}/principal":{"put":{"tags":["chairman"],"summary":"Bind Board Seat Principal","description":"Bind a board seat to the one principal that answers for it (issue\n#2218). Same shape as the remit route above: chairman-gated, and the row\nrecords who bound it and when (`principal_bound_by`/`_at`), so the bind is\nnever again a raw column write nobody can account for. An agent principal\nmust have a live human sponsor (#2231); one principal, one seat.","operationId":"bind_board_seat_principal_api_chairman_board_seats__seat_key__principal_put","parameters":[{"name":"seat_key","in":"path","required":true,"schema":{"type":"string","title":"Seat Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatPrincipalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Bind Board Seat Principal Api Chairman Board Seats  Seat Key  Principal Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/board/papers":{"get":{"tags":["chairman"],"summary":"List Board Papers","description":"Every board paper still waiting for somebody.\n\n⛔ALMOST ALWAYS EMPTY, AND THAT IS THE CORRECT STATE. No decision class\ncarries `board_reviews = true`, so nothing reaches the board until he\nnames one here (the remit route above). An empty list is the honest\nanswer, not a sign the chain is broken.","operationId":"list_board_papers_api_chairman_board_papers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Board Papers Api Chairman Board Papers Get"}}}}}}},"/api/chairman/board/papers/{ballot_id}":{"get":{"tags":["chairman"],"summary":"Get Board Paper","description":"One paper, with who answered it and how each answer reached us.","operationId":"get_board_paper_api_chairman_board_papers__ballot_id__get","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Board Paper Api Chairman Board Papers  Ballot Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/board/papers/{ballot_id}/seat-framing":{"post":{"tags":["chairman"],"summary":"Paste Seat Framing","description":"Paste in a seat's blind read, on its behalf.\n\n⛔RECORDED AS A PASTE, WITH HIS NAME ON IT, AND NEITHER HALF IS OPTIONAL.\n`answers.chair_pasted` is the one spelling of that pair -- a caller that\nremembered the source and forgot the principal would be refused by the\ndatabase (0528) rather than by review, which is late.","operationId":"paste_seat_framing_api_chairman_board_papers__ballot_id__seat_framing_post","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatAnswerFramingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Paste Seat Framing Api Chairman Board Papers  Ballot Id  Seat Framing Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/board/papers/{ballot_id}/seat-positions":{"post":{"tags":["chairman"],"summary":"Paste Seat Positions","description":"Paste in a seat's vote, on its behalf.","operationId":"paste_seat_positions_api_chairman_board_papers__ballot_id__seat_positions_post","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatAnswerPositionsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Paste Seat Positions Api Chairman Board Papers  Ballot Id  Seat Positions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/board/papers/{ballot_id}/ruling":{"post":{"tags":["chairman"],"summary":"Rule On Board Paper","description":"The chair breaks a paper the board could not answer itself.\n\n⛔ONLY OUT OF `awaiting_chair`, and the ruling is attributed to him. See\n`board/answers.py::chair_rules` for the three refusals this carries --\nincluding that a paper with no position from Fable's seat is refused here\ntoo, not only at the point the room closes.","operationId":"rule_on_board_paper_api_chairman_board_papers__ballot_id__ruling_post","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChairRulingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Rule On Board Paper Api Chairman Board Papers  Ballot Id  Ruling Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/board/papers/{ballot_id}/answer-window":{"put":{"tags":["chairman"],"summary":"Set Board Answer Window","description":"How long this paper waits before silence becomes `expired`.\n\n⛔THE WINDOW IS ALWAYS HIS, NEVER A DEFAULT. Nobody has set a number of\nhours the board gets, and inventing one here would be a threshold built\nfrom nothing -- a guess wearing a measurement's clothes. A paper with no\nwindow never expires on its own; it waits until somebody answers it.\n\n⛔AND EXPIRY IS NOT A REJECTION. When the window passes, the paper resolves\n`expired`: a fact about us having stopped waiting. It names no option and\napproves nothing.","operationId":"set_board_answer_window_api_chairman_board_papers__ballot_id__answer_window_put","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerWindowRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Board Answer Window Api Chairman Board Papers  Ballot Id  Answer Window Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/chairman/streak":{"get":{"tags":["chairman"],"summary":"Get Streak","description":"Days run without an operational decision from the chairman, the target\nit counts toward, and the five guards it must stay inside while it counts.\nA board review never resets it; a decision does.","operationId":"get_streak_api_chairman_streak_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Streak Api Chairman Streak Get"}}}}}}},"/api/chairman/ladder":{"get":{"tags":["chairman"],"summary":"Get Ladder","description":"The autonomy ladder: each decision class's rung and blind-agreement\nrate (sighted excluded), and per-seat calibration including the chairman's\nown row. No promote button -- promotion is mechanical, so nothing here\nwrites a rung. A class with no blind decisions reads 'nothing to measure\nyet', never 0%.","operationId":"get_ladder_api_chairman_ladder_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ladder Api Chairman Ladder Get"}}}}}}},"/api/board/papers/{ballot_id}/packet":{"get":{"tags":["board"],"summary":"Read Blind Packet","description":"The case, as this seat is allowed to see it.\n\n⛔THIS DOOR CANNOT SERVE THE SIGHTED PACKET, AND THAT IS THE MECHANISM.\n`board_ballots.evidence_pack` carries the proposer's recommendation, its\nconfidence and every option's own confidence, because the approval\nfingerprint has to cover all of them (`domain.evidence_pack_for`). It is\nnever returned here. `blind_case_from_pack` is an ALLOWLIST over two keys,\nso no field a future writer adds to the pack can reach a seat through this\nroute by default -- and `BlindOption` has no `confidence` field at all, so\nthere is nothing for a later writer to populate.\n\n⭐THE CONFIDENCES ARE THE RECOMMENDATION WRITTEN IN NUMBERS. The\nrecommended option is by construction the one the proposer is most\nconfident in, so shipping each option's confidence would hand the seat the\nanswer while looking like context. AMENDMENT 4 names this as the half a\nreimplementation gets wrong.\n\n⛔IT IS STILL SERVED AFTER THE REVEAL, deliberately. A seat that has framed\ngoes on to vote on the union of options, and refusing the packet at that\npoint would leave it voting on labels alone. What it never carries, at any\npoint, is the proposer's pick.","operationId":"read_blind_packet_api_board_papers__ballot_id__packet_get","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Blind Packet Api Board Papers  Ballot Id  Packet Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/board/papers/{ballot_id}/seat-out":{"post":{"tags":["board"],"summary":"Report Seat Is Out","description":"This seat cannot run its top model for this paper. Hand it to the chair.\n\n⛔THE ONLY THING THE SEAT MAY DO WHEN IT IS OUT, and the reason this route\nexists rather than the runner simply going quiet: a seat that says nothing\nis indistinguishable from a seat that is still thinking, and the paper\nwould sit until its window expired -- which records that WE stopped\nwaiting, not that a person now owns it.\n\n⛔IT NEVER WRITES A VOTE AND CANNOT. There is no option label on this body\nand no path from here into `cast_positions`; being out is not a stance.","operationId":"report_seat_is_out_api_board_papers__ballot_id__seat_out_post","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatIsOutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Report Seat Is Out Api Board Papers  Ballot Id  Seat Out Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/board/seats/{seat_key}/runner-status":{"post":{"tags":["board"],"summary":"Report Runner Status","description":"A seat's runner says whether it can answer, and why (issue #3555).\n\n⛔DELIBERATELY NOT `_require_the_bound_seat`. That binding stops one\ncredential FRAMING OR VOTING for two seats; this route can do neither.\nIt exists for the runner that has no credential of its own -- board seat\ntwo with `ROOST_API_TOKEN_BOARD_SEAT_TWO` unset is the case that went\nsilent for a week -- and can only speak with the shared one. The grant\nstill gates it (`rooster:board:seat`), and the seat must exist.\n\n⛔WHAT IT DOES: store the report. `board/seat_worker.py::examine` reads a\nfresh non-`ready` report and writes the refusal row and the chair\nhand-over with this reason. It writes no answer and moves no paper itself.","operationId":"report_runner_status_api_board_seats__seat_key__runner_status_post","parameters":[{"name":"seat_key","in":"path","required":true,"schema":{"type":"string","title":"Seat Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunnerStatusRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Report Runner Status Api Board Seats  Seat Key  Runner Status Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/board/papers":{"get":{"tags":["board"],"summary":"List Open Papers","description":"Every board paper still waiting for somebody.\n\nA seat's own inbox. Almost always empty: nothing reaches the board until\nFrederick names a class, and an empty list is the honest answer rather\nthan a sign anything is wrong.","operationId":"list_open_papers_api_board_papers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Open Papers Api Board Papers Get"}}}}}}},"/api/board/papers/{ballot_id}/framing":{"post":{"tags":["board"],"summary":"Submit Seat Framing","description":"This seat's blind read of the paper.\n\n⛔`answer_source` IS FIXED TO `seat_tool` HERE AND IS NOT A FIELD ON THE\nBODY. If a caller could declare its own provenance, a pasted position\ncould be posted through this door claiming to be the seat's own -- which\nis precisely the fact 0528 exists to keep straight. The door decides the\nprovenance; the body cannot.\n\n⛔AND `seat_key` IS NOW VERIFIED AGAINST THE CALLER, NOT JUST READ FROM\nIT (FH-09). See `_require_the_bound_seat`.","operationId":"submit_seat_framing_api_board_papers__ballot_id__framing_post","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FramingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Submit Seat Framing Api Board Papers  Ballot Id  Framing Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/board/papers/{ballot_id}/positions":{"post":{"tags":["board"],"summary":"Cast Seat Positions","description":"This seat's vote on the revealed ballot.\n\nThe response is the PAPER, not an acknowledgement -- a seat that has just\nanswered needs to know whether the room is now finished, still waiting on\nthe other seat, or already in the chair's hands, and an `{\"ok\": true}`\nwould make it ask again to find out.\n\n⛔`seat_key` IS VERIFIED AGAINST THE CALLER BEFORE ANY VOTE IS WRITTEN\n(FH-09). See `_require_the_bound_seat`.","operationId":"cast_seat_positions_api_board_papers__ballot_id__positions_post","parameters":[{"name":"ballot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ballot Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PositionsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Cast Seat Positions Api Board Papers  Ballot Id  Positions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/quality/reviews/open":{"get":{"tags":["customer_service"],"summary":"List Open Reviews","description":"What the seat may read now, for ONE department. An empty list is the\nordinary answer: nothing settled since the last pass.","operationId":"list_open_reviews_api_cs_quality_reviews_open_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Limit"}},{"name":"department","in":"query","required":false,"schema":{"type":"string","default":"customer_service","title":"Department"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Open Reviews Api Cs Quality Reviews Open Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/quality/reviews/{review_id}/packet":{"get":{"tags":["customer_service"],"summary":"Read Review Packet","description":"The packet, and the lease that comes with reading it. Which\ndepartment's packet (ticket or contractor-week) is decided entirely by\n`service.claim_packet` from the row's own `department` -- this route\ndoes not need to know.","operationId":"read_review_packet_api_cs_quality_reviews__review_id__packet_get","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Review Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Review Packet Api Cs Quality Reviews  Review Id  Packet Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/quality/reviews/{review_id}/answer":{"post":{"tags":["customer_service"],"summary":"Answer Review","description":"The verdict. 422 for a malformed answer or a downgraded run (both\nrecorded as a seat-out on the row first); 409 if the row was already\nanswered.\n\n⛔AFTER A FULFILLMENT ROW FILES, THE FEEDBACK HOOK RUNS HERE, AT THE\nCOMPOSITION EDGE -- never inside `service.record_answer` itself, which\nstays department-blind on purpose (CS's own calibration gate means a\nCS row must never trigger a card or a DM; see `fulfillment.quality.\nfeedback`'s own module docstring for why this is a route-layer decision\nand not a service-layer one).","operationId":"answer_review_api_cs_quality_reviews__review_id__answer_post","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Review Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Answer Review Api Cs Quality Reviews  Review Id  Answer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cs/quality/reviews/{review_id}/seat-out":{"post":{"tags":["customer_service"],"summary":"Review Seat Out","description":"The seat could not answer: a plan limit, a timeout, an empty run.","operationId":"review_seat_out_api_cs_quality_reviews__review_id__seat_out_post","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Review Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatOutIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Review Seat Out Api Cs Quality Reviews  Review Id  Seat Out Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/quality/reviews/{review_id}/acknowledge":{"post":{"tags":["fulfillment"],"summary":"Acknowledge Review","description":"The contractor's own answer to their own FAIL card -- never on\nsomebody else's behalf. 403 if the caller is not the reviewed\ncontractor; 404 if the row is not a FAIL waiting on this answer (it\nwas a PASS, it is not fulfillment's, or it was already answered).","operationId":"acknowledge_review_api_fulfillment_quality_reviews__review_id__acknowledge_post","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Review Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcknowledgeIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Acknowledge Review Api Fulfillment Quality Reviews  Review Id  Acknowledge Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t/o/{token}.gif":{"get":{"tags":["tracking"],"summary":"Open Pixel","description":"The open pixel.\n\n⛔ALWAYS RETURNS THE SAME IMAGE, whatever the token. A 404 on an unknown\ntoken turns this into an oracle that tells a stranger which tokens are\nreal, and a stranger who can distinguish real tokens can walk the send log.\nUnknown, malformed and revoked all look identical from outside.","operationId":"open_pixel_t_o__token__gif_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t/c/{token}/{position}":{"get":{"tags":["tracking"],"summary":"Click Redirect","description":"The click redirect: record, then send them where they were going.\n\n⛔THE DESTINATION COMES FROM THE DATABASE, NEVER THE REQUEST. There is no\n`?url=` parameter and there must never be one -- that is an open redirect,\nand an open redirect on the domain we are asking sheriffs' offices to trust\nis a phishing kit with our name on it. Only URLs we ourselves put in an\nemail are reachable, and the schema refuses anything that is not absolute\nhttps.\n\nAn unknown token sends them to the public site rather than an error page.\nA person who clicked a link in a real email should never land on a stack\ntrace because a row was cleaned up -- and, as with the pixel, the failure\nmust not be distinguishable from a success.","operationId":"click_redirect_t_c__token___position__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"position","in":"path","required":true,"schema":{"type":"integer","title":"Position"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shifts/clock-in":{"post":{"tags":["shifts"],"summary":"Clock In","description":"Start your own shift. Never anybody else's -- the principal comes from\nthe session, and there is no parameter to name a different person.\n\nA THIN CALLER of `service.clock_in`, the one clock-in. Frederick,\n2026-10-04: \"Why isn't it only one flow for her floor, floor block and\nclocking out? It doesn't make sense for it to be two paths.\" This door\nand `POST /api/floor/clock-in` differ only in what they say to the\nperson: the device gate and the words of a refusal, and how an error is\nspelled on the wire. Whether the person holds a floor block, whether a\ntub count is owed, what the early/late window is measured against --\nnone of that is decided here.\n\n⛔WORKSTATION-ONLY ROLES CLOCK IN AT A WORKSTATION, THROUGH THE\nCONTRACTOR'S OWN GATE. Frederick, 2026-09-22: \"the people in Vegas can\nonly clock in from the workstations in Vegas.\" `floor_blocks.clock.\ncheck_device_gate` holds a floor person to the office network or a\nregistered workstation; `floor_clock.workstation_surface_for` is the ONE\nquestion of who that covers -- an on-site ROLE (`shifts.domain.\nON_SITE_ROLES`) and, since 2026-10-04, a floor CONTRACTOR, who this door\nnever asked (they hold no on-site role; the Today bar sent them to\n/floor/clock, which hid the hole). Customer service is not on site and is\nnot asked: \"customer service people can clock in from home.\" Every\nrefusal is recorded (`enforce_device_gate`) so William sees who tried.","operationId":"clock_in_api_shifts_clock_in_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ClockInRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Clock In Api Shifts Clock In Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shifts/clock-out":{"post":{"tags":["shifts"],"summary":"Clock Out","description":"End your own shift. A THIN CALLER of `service.clock_out`, the one\nclock-out: it closes the shift AND the person's floor block, and asks a\nfloor contractor why a short block was short, exactly as the workstation's\nown button does (Frederick, 2026-10-04: \"It doesn't make sense for it to\nbe two paths\").","operationId":"clock_out_api_shifts_clock_out_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClockOutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Clock Out Api Shifts Clock Out Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/answer":{"post":{"tags":["voice"],"summary":"Inbound Answer","operationId":"inbound_answer_api_voice_inbound_answer_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Answer Api Voice Inbound Answer Post"}}}}}}},"/api/voice/prompt/{name}":{"get":{"tags":["voice"],"summary":"Voice Prompt","description":"The recording `<Play>` fetches: a greeting or menu line in his voice.\n\n⛔PUBLIC BY NECESSITY. Plivo fetches this with no session, like every\nother `/inbound/*` route. What it serves is the same fixed marketing copy\nanybody hears by ringing the number -- no caller, no account, nothing\npersonal -- so there is nothing here a signature would be protecting.\n\nA cold cache answers 404 AND starts recording, rather than recording\ninline: `_sayer` only ever points `<Play>` at a prompt that is already\nready, so a 404 here means somebody asked out of turn, not that a caller\nis waiting on it.","operationId":"voice_prompt_api_voice_prompt__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Voice Prompt Api Voice Prompt  Name  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/menu/{level}/{lang}":{"post":{"tags":["voice"],"summary":"Inbound Menu","description":"One keypress in the opener tree.\n\n⛔EVERY BRANCH ENDS ON ROOSTER. There is no path out of this route that\nleaves a caller in a menu, replays a prompt at them, or hangs up. An\nunexpected digit, a missing digit, a level we do not recognise: all of\nthem connect. The old Dialpad tree is the thing 70% of callers abandoned\ninside, and the one rule that keeps this from becoming that is that the\nmenu can only ever move a caller FORWARD.","operationId":"inbound_menu_api_voice_inbound_menu__level___lang__post","parameters":[{"name":"level","in":"path","required":true,"schema":{"type":"string","title":"Level"}},{"name":"lang","in":"path","required":true,"schema":{"type":"string","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Menu Api Voice Inbound Menu  Level   Lang  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/connect/{lang}/{lane}":{"post":{"tags":["voice"],"summary":"Inbound Connect","description":"The recording notice, then the stream. The last thing before Rooster.\n\nSeparate from `/inbound/answer` because a `<Redirect>` needs somewhere to\nland, and because the notice must be the element immediately before the\nstream opens no matter WHICH branch of the tree got here.","operationId":"inbound_connect_api_voice_inbound_connect__lang___lane__post","parameters":[{"name":"lang","in":"path","required":true,"schema":{"type":"string","title":"Lang"}},{"name":"lane","in":"path","required":true,"schema":{"type":"string","title":"Lane"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Connect Api Voice Inbound Connect  Lang   Lane  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/stream-status":{"post":{"tags":["voice"],"summary":"Inbound Stream Status","description":"The `<Stream>` verb's own `statusCallbackUrl` (build task's XML\nshape): Plivo posts stream-lifecycle events here (started/stopped/\nerrored) separately from the call-level webhooks above. Signature-gated\nthe same open-when-unconfigured way as every other inbound route in this\nmodule.\n\n⛔\"started\" IS THE ONE EVENT THIS ROUTE ACTS ON, because it is the first\nmoment a caller can be told apart from one still ringing in the hold\nqueue. An inbound row is 'ringing' from `/inbound/answer` until hangup,\nand nothing used to transition it when the media stream connected --\nwhich means the hold queue (inbound + 'ringing' + under 30 minutes,\n`service.list_calls`) reads every live Rooster conversation as a caller\non hold, and the desk's Answer button (`answer_here`) would redirect\nsomebody mid-sentence onto hold music and then the desk. Every other\nevent (stopped/errored) is still only logged: the voice_calls row is\nalready the record of what actually happened to the call\n(recording/DTMF/callback fields), and a redundant status ledger would be\na second place to look for the same fact.","operationId":"inbound_stream_status_api_voice_inbound_stream_status_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Inbound Stream Status Api Voice Inbound Stream Status Post"}}}}}}},"/api/voice/inbound/recording":{"post":{"tags":["voice"],"summary":"Inbound Recording","description":"Plivo hands back a failsafe voicemail. Answers XML, never JSON.\n\nThe response is what Plivo plays next, because this URL is the `<Record>`\nelement's `action`. See `_recording_taken_xml` for the outage that taught\nus the difference between an `action` and a `callbackUrl`.","operationId":"inbound_recording_api_voice_inbound_recording_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Recording Api Voice Inbound Recording Post"}}}}}}},"/api/voice/inbound/transcription":{"post":{"tags":["voice"],"summary":"Inbound Transcription","operationId":"inbound_transcription_api_voice_inbound_transcription_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Inbound Transcription Api Voice Inbound Transcription Post"}}}}}}},"/api/voice/inbound/hangup":{"post":{"tags":["voice"],"summary":"Inbound Hangup","operationId":"inbound_hangup_api_voice_inbound_hangup_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Inbound Hangup Api Voice Inbound Hangup Post"}}}}}}},"/api/voice/outbound-channel":{"get":{"tags":["voice"],"summary":"Outbound Channel","operationId":"outbound_channel_api_voice_outbound_channel_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundChannelResponse"}}}}}}},"/api/voice/may-we-ring":{"get":{"tags":["voice"],"summary":"May We Ring Them","description":"Whether this number is inside its own callable window, and what to say.\n\n⛔THE BROWSER COULD NOT ASK THIS BEFORE, WHICH IS WHY THE DIALPAD WARNED\nNOBODY. No endpoint exposed a ringability verdict, so a warning would have\nmeant a second copy of \"is this a decent hour\" in TypeScript -- a literal\nthat drifts out of agreement with `callable_hours`, which owns the rule.\n\nSo the rule stays where it is and this route carries its answer, sentence\nincluded. Same principle as `Ringable.reason`: this module already owns\nthe words an agent is shown about an hour.\n\n⛔A READ, AND ONLY A READ. It places no call and changes nothing; the\nrefusal that actually stops a CALLBACK is at the dial in\n`/outbound/from-browser`, because a screen's copy of the hour is as old as\nits last fetch and a hidden button is a convention rather than an\ninvariant. This exists so a manual dial can be an informed override,\nwhich is what was asked for -- not so the browser can police itself.","operationId":"may_we_ring_them_api_voice_may_we_ring_get","parameters":[{"name":"phone","in":"query","required":true,"schema":{"type":"string","title":"Phone"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheirClockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/browser-token":{"get":{"tags":["voice"],"summary":"Browser Token","operationId":"browser_token_api_voice_browser_token_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowserTokenResponse"}}}}}}},"/api/voice/browser-call-answered":{"post":{"tags":["voice"],"summary":"Record Browser Call Answered","description":"The browser telling us a call is up, so the call has a row to belong to.\n\n⛔WITHOUT THIS A BROWSER-DIALLED CALL COULD NOT BE DISPOSITIONED AT ALL.\nMeasured on ticket 60884, 2026-09-22: the agent finished a callback,\nopened the disposition prompt and got a red refusal naming a CallUUID\nthat exists in no `voice_calls` row -- not as `call_uuid`, not as `id`.\n`record_disposition` was right to refuse; there was nothing to attach to.\n\n⛔THE ROW WAS SUPPOSED TO COME FROM THE ANSWER URL AND FOR THIS DESK IT\nDOES NOT ARRIVE. `/outbound/from-browser` does write one -- but only\n`if ticket_id is not None and call_uuid`, so a dial carrying no ticket\nheader writes nothing; and the route runs at all only while Plivo's\nBROWSER application points its answer URL at us, which that route's own\ndocstring calls out as \"a Plivo change, not a deploy\". Its insert is also\nwrapped in a bare `except Exception` that logs and continues, because an\nagent is waiting on that XML. Three ways for the row to be missing, all\nof them silent, and the agent finds out at the one moment they cannot act.\n\nSo the fact is reported from the one place that always has it: the\nbrowser, which is holding the CallUUID because the SDK just handed it\nover on `onCallAnswered`. Same shape as `/origination-failed` above --\nthe softphone already tells us about the calls it could NOT place, and\nthis is the other half of that sentence.\n\n⛔NOT `reconcile_the_call_uuid`, AND THE PRECONDITION IS WHY. The bridge\npath reconciles because Plivo's REST API answers with a `request_uuid`\nand every later webhook carries a different `CallUUID`: the row is born\nwearing a placeholder and the real id arrives afterwards. A browser leg\nhas no such split -- the SDK hands over the real CallUUID at once, and\n`/outbound/from-browser` already inserts with it -- so there is no\nplaceholder to replace. Worse, reconciling here would be actively wrong:\nit keys on \"the ONE unreconciled ringing row on this ticket\", and browser\nrows satisfy `call_uuid = request_uuid` forever, so a NEW call would\nrewrite the identity of an OLDER stuck attempt. Ticket 60884 is carrying\nfive rows still at 'ringing'; this conversation's disposition and\nrecording would have landed on one of them.\n\n⛔ALWAYS 200, AND THE BOOLEAN SAYS WHETHER IT LANDED. The agent is live\non a call; a bookkeeping failure must never become something they see.\n`recorded: false` is for us, in the log beside it.\n\n⛔EXCEPT A REFUSED DIAL, WHICH ANSWERS `refused` WITH THE SENTENCE\n(2026-09-24). That one IS for the agent: the dial they just pressed was\nstopped by the calling hours, and the softphone shows the same words\nthe call leg spoke, instead of offering a disposition for a call that\nnever reached anybody.","operationId":"record_browser_call_answered_api_voice_browser_call_answered_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowserCallAnsweredRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"string"}]},"type":"object","title":"Response Record Browser Call Answered Api Voice Browser Call Answered Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/origination-failed":{"post":{"tags":["voice"],"summary":"Record Origination Failed","description":"The browser telling us it could not place a call.\n\n⛔THIS ROUTE EXISTS BECAUSE THE FACT NEVER REACHED US. On 2026-09-18 five\nof six agents could not place a call for a whole morning -- one shared\nPlivo endpoint, one call at a time -- and we learned about it from a\nSlack message. `SoftphonePanel.tsx`'s `onCallFailed` knew the reason, told\nthe agent, and told nobody else. There was no row to fail to write.\n\n⛔ALWAYS 200, EVEN WHEN THE WRITE FAILS. This is reported by somebody who\nhas just failed to reach a customer; the one thing worse than losing the\nrecord is turning it into a second visible failure. `record` swallows and\nlogs, and the agent's own error message never depended on this landing.","operationId":"record_origination_failed_api_voice_origination_failed_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OriginationFailedRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Record Origination Failed Api Voice Origination Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/bridge-caller":{"post":{"tags":["voice"],"summary":"Record Bridge Call Caller","description":"The browser telling us who is on a cell-bridge call, the moment\n`ring_them_back`/`outbound/cell-bridge` hands back a `call_uuid`.\n\nFrederick, 2026-09-25 (#2186): Cindy made a callback on the cell bridge\nand got no disposition screen, because that screen is wired to the\nsoftphone's own Plivo SDK and a bridge call never touches it -- Plivo\nrings the account manager's own phone, and the browser never learns the\ncall started or ended. If the call ever goes undispositioned long enough\nto raise a Today card (`bridge_disposition.raise_disposition_cards`),\nthis is how that card knows who to put it in front of instead of\nfalling straight to the ticket's queue pool.\n\n⛔SAME POSTURE AS `record_browser_call_answered`/`record_origination_\nfailed` just above: self-reported, best-effort, and ALWAYS 200. Nobody\nis on a live call waiting for this response the way they are for those\ntwo, but `TicketDetailPage.ringBack` fires it fire-and-forget right\nafter a successful dial and must never be made to handle a failure here\nas if the callback itself had failed.\n\n⛔ONLY `ringBack` FIRES THIS TODAY (Fable's review, F9, 2026-09-25 --\ncorrected the same day this route first shipped, which claimed the\nkeypad's cell-bridge dial did too). `SoftphonePanel.tsx`'s\n`placeCellBridgeCall` does not call it, so a keypad bridge dial placed\nFROM a ticket still gets a Today card if it goes undispositioned -- it\njust always falls to the ticket's queue pool rather than to a name.\nLeft open (not silently wired) rather than claimed fixed.","operationId":"record_bridge_call_caller_api_voice_outbound_bridge_caller_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeCallCallerRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Record Bridge Call Caller Api Voice Outbound Bridge Caller Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls/{call_uuid}/colleagues":{"get":{"tags":["voice"],"summary":"Call Colleagues","description":"Who this call can be handed to right now.\n\nFrederick, 2026-09-10: \"any user should be able to transfer calls to any\nother user that's available.\" So this is not scoped to account managers or\nto a department -- it is everybody we can actually reach, with whoever is\nclocked in first and the rest flagged rather than hidden.","operationId":"call_colleagues_api_voice_calls__call_uuid__colleagues_get","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ColleagueOut"},"title":"Response Call Colleagues Api Voice Calls  Call Uuid  Colleagues Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls/{call_uuid}/disposition":{"post":{"tags":["voice"],"summary":"Record Call Disposition","description":"How this call ended, said by the person who was on it.\n\nFrederick, 2026-09-17: \"At the end of the call, it should force you to do\na disposition on the ticket... And then that also will tell Rooster how\nthe call ended.\"\n\n⛔THE CLAIM IS ALWAYS RECORDED; THE TICKET MOVING IS A CONSEQUENCE. 77 of\n142 calls in production carry no ticket at all, because somebody rang and\nasked nothing -- so a disposition that required one would not exist for\nthe majority of calls, and the Rooster-learning half of this would be\nmissing its larger part.\n\n⛔AND IT APPENDS RATHER THAN REPLACING. If Rooster ever dispositions a\ncall and a human later disagrees, BOTH rows survive -- that disagreement\nis the training signal this exists to collect.\n\n⛔THE PATH CARRIES `call_uuid`, THE SAME ID AS ITS FOUR SIBLINGS. It was\n`{call_id}` -- the only one of the five `/calls/{...}` routes spelled\nthat way -- and it meant the internal primary key, which no browser has.\nEvery caller of this route sends what the Plivo SDK gave it, so the route\nthat asked for something else answered 404 to every well-formed request.\n\n422 on a word outside the seven an agent may choose (`voice.domain.\nCHOOSABLE_CALL_DISPOSITIONS` -- as of 2026-09-22, this is no longer the\nsame six as `CALL_DISPOSITIONS` itself, which still accepts two retired\nwords for historical rows), rather than letting the database's CHECK\nanswer with a 500: an agent picking from a list should never see a\nserver error, and the message names the word.","operationId":"record_call_disposition_api_voice_calls__call_uuid__disposition_post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__modules__voice__routes__DispositionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DispositionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls/{call_uuid}/transfer":{"post":{"tags":["voice"],"summary":"Transfer Call","description":"Hand this call to somebody else.\n\n⛔TWO OUTCOMES AND BOTH ARE ALREADY BUILT. Frederick: \"either that person\nanswers, or Rooster collects all the information and schedules their\ncallback.\" This reuses the agent's own transfer path, whose dial-action\ncallback already returns an unaccepted caller to Rooster to do exactly\nthat -- so the promise is kept by the same code that keeps it for her.","operationId":"transfer_call_api_voice_calls__call_uuid__transfer_post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoffRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoffResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls/{call_uuid}/add":{"post":{"tags":["voice"],"summary":"Add To Call","description":"Pull a third person into this call without ending it.\n\nFrederick: \"sometimes an agent needs to bring somebody else on the call to\nhelp them with the call.\"","operationId":"add_to_call_api_voice_calls__call_uuid__add_post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoffRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandoffResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/conference/{room}":{"post":{"tags":["voice"],"summary":"Inbound Conference","description":"The room a call and the colleague joining it both land in.\n\n⛔NOT SIGNATURE-GATED THE USUAL WAY AND THAT IS DELIBERATE: this is\nfetched for the OUTBOUND leg too, which Plivo signs against a different\nURL than the inbound webhooks. The room name carries a call uuid we\ngenerated and nothing else -- joining it early gets you a conference with\nnobody in it.\n\n⛔AND THE COLLEAGUE'S LEG RECORDS ITSELF HERE (ISS-1239). `handoff.\nbring_colleague_in` writes an outbound row for that leg and this is its\nanswer URL, so it is a real conversation on the desk's ticket that\nnothing ever asked Plivo to record: `a_call_that_should_have_been_\nrecorded` counted every one. Only a leg whose row nobody has asked about\nis recorded; the call being moved in already was, and a leg with no row\nof ours (a softphone dial's customer) has nowhere for audio to land. Asked\nonce the XML is out, never inside it (ISS-1031, `_record_once_it_is_up`).\n\n⛔THE ROOM IS OPEN, THE ROW IS NOT. Finding the leg's row renames it off\nits request uuid, which is a write, so that half runs only for a request\nPlivo signed. An unsigned one still gets the room, exactly as before.","operationId":"inbound_conference_api_voice_inbound_conference__room__post","parameters":[{"name":"room","in":"path","required":true,"schema":{"type":"string","title":"Room"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Conference Api Voice Inbound Conference  Room  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/scoreboard":{"get":{"tags":["voice"],"summary":"Phone Scoreboard","description":"Did the phone beat Dialpad, on the same numbers.\n\nPHONE_SYSTEM_PLAN.md section 9 asks for these to be live metrics rather\nthan a one-time claim. They are deliberately NOT one of the four\ncustomer-service tiles: that table allows exactly four per role and all\nfour are taken, so claiming a slot would silently evict somebody's\nexisting measure. Which of them the phone is worth more than is a\ndecision for Frederick, not for this route.","operationId":"phone_scoreboard_api_voice_scoreboard_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneScoreOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls":{"get":{"tags":["voice"],"summary":"List Calls","description":"The softphone panel's call queues (build task: \"call queues beside\nthe softphone: on-hold / callbacks / recent\"; `voice_calls` table\nalready exists). `status` repeats to filter\n(`?status=ringing` for \"on hold\", `?status=completed&status=failed&...`\nfor \"recent calls\") -- omitted, returns the most recent calls of any\nstatus. Gated `cs:voice`, the same page-level grant as the rest of this\npanel.\n\n`phone`, since Frederick, 2026-09-16: \"calls need to be able to search by\ntelephone number ... there's really no way to search for calls\" -- typed\nwith or without the country code, punctuated or not, full or partial\n(`service.list_calls`'s own docstring has the matching rule). Never\nvalidated beyond that: an unparseable string just matches nothing, the\nsame as any other search box that comes back empty.","operationId":"list_calls_api_voice_calls_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Status"}},{"name":"phone","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Calls Api Voice Calls Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls/directed-to-me":{"get":{"tags":["voice"],"summary":"Calls Directed To Me","description":"Live calls a colleague is transferring to ME, and to nobody else.\n\n⛔THIS REPLACES A BROADCAST. Frederick, 2026-09-19: \"No, we don't take\nlive calls. So there's no reason for a call to be broadcasted. All calls\nare callback tickets. However, the only time that bar should show up is on\nyour page when someone's trying to transfer you a call... outside of that,\nit should not be shown to everyone because we don't answer live calls.\"\n\nThe desk strip used to read `GET /calls?status=ringing` -- inbound and not\nyet 'in-progress' -- and draw it for all seventeen holders of `cs:voice`.\nThat was never a queue of people waiting: `ON_HOLD_STATUS` IS the string\n`'ringing'`, the state every answered inbound call starts in while Rooster\nis still connecting, so the strip alarmed on every single caller.\n\n⛔GATED ON BEING THE TARGET, NOT ON HOLDING `cs:voice`. The grant is still\nrequired -- this is a phone screen -- but it decides whether the person may\nask the question at all, never whose calls come back. The answer is always\nand only this principal's own, because the row names them; there is no\nparameter here that could widen it.\n\n⛔AND ONLY THE ONES STILL NEEDING SOMEBODY TO PRESS ANSWER. A transfer\ndelivered to a colleague's own American cell phone carries\n`transfer_number` and needs no button -- their phone is simply ringing\n(`handoff.transfer_call_to`). The browser branch leaves that column NULL,\nwhich is what the desk reads to decide whether to draw anything.","operationId":"calls_directed_to_me_api_voice_calls_directed_to_me_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Calls Directed To Me Api Voice Calls Directed To Me Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/caller-match":{"get":{"tags":["voice"],"summary":"Caller Match","description":"The softphone panel's \"Do it on the call\" account context: matches a\nconnected call's caller ID the SAME way the inbound webhook does, so a\nhuman agent sees the identical org/contact resolution the ticket (if\nany) was filed against. No new lookup beyond this -- the panel composes\nthe rest of the account view from the EXISTING orgs/contacts/tickets\nendpoints (GET /api/orgs/organizations/{id}, GET /api/orgs/contacts/{id},\nGET /api/cs/tickets).","operationId":"caller_match_api_voice_caller_match_get","parameters":[{"name":"phone","in":"query","required":true,"schema":{"type":"string","title":"Phone"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallerMatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/transfer/{call_uuid}":{"post":{"tags":["voice"],"summary":"Inbound Transfer","description":"The XML Plivo fetches once the agent has asked for a transfer.\n\n⛔`confirmSound` IS THE WHISPER AND `confirmKey` IS THE ACCEPT. Plivo\nrings the manager, plays whatever that URL returns, and only bridges the\ncaller once the key is pressed -- which is exactly the flow Frederick\ndescribed, and is why this is call screening rather than a plain dial.\n\nA dial with no confirm would connect a caller to a voicemail box and\nreport success, because from our side an answered call and an answering\nmachine are the same event.","operationId":"inbound_transfer_api_voice_inbound_transfer__call_uuid__post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Transfer Api Voice Inbound Transfer  Call Uuid  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/transfer-hold/{call_uuid}":{"post":{"tags":["voice"],"summary":"Inbound Transfer Hold","description":"What the caller hears while we ring their account manager.\n\n`dialMusic` takes a URL returning XML, so this is a real route rather than\nan audio file, which is the better shape anyway: it can say WHO is being\nrung, in the caller's own language.\n\n⛔IT MUST NOT OUTLAST THE DIAL, AND IT DOES NOT NEED TO. Plivo stops\nplaying this the instant the legs bridge or the dial gives up, so the\nloop is bounded by the dial timeout above rather than by anything here.","operationId":"inbound_transfer_hold_api_voice_inbound_transfer_hold__call_uuid__post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Transfer Hold Api Voice Inbound Transfer Hold  Call Uuid  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/whisper/{call_uuid}":{"post":{"tags":["voice"],"summary":"Inbound Whisper","description":"What the manager hears while the caller waits.\n\n⛔THIS ARRIVES AS ITS OWN HTTP REQUEST WITH NOTHING BUT A CALL ID. No\nwebsocket, no conversation, no memory -- which is exactly why the whisper\nis written to the call row BEFORE the transfer starts (migration 0427).\nComposing it here would mean composing it from nothing.","operationId":"inbound_whisper_api_voice_inbound_whisper__call_uuid__post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Whisper Api Voice Inbound Whisper  Call Uuid  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/transfer-done/{call_uuid}":{"post":{"tags":["voice"],"summary":"Inbound Transfer Done","description":"Plivo's report on how the dial ended.\n\n⛔`DialBLegHangupCause`/`DialStatus` IS THE ONLY THING THAT DISTINGUISHES\nA HUMAN TAKING THE CALL FROM A DIAL THAT RANG OUT. `completed` here means\nthe confirm key was pressed and the legs were bridged; anything else and\nnobody took it, and the caller must not be counted as connected.","operationId":"inbound_transfer_done_api_voice_inbound_transfer_done__call_uuid__post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Transfer Done Api Voice Inbound Transfer Done  Call Uuid  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/prompts":{"get":{"tags":["voice"],"summary":"List Menu Recordings","description":"Every fixed line the menu says, and whose voice is saying it.","operationId":"list_menu_recordings_api_voice_prompts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PromptRecordingOut"},"type":"array","title":"Response List Menu Recordings Api Voice Prompts Get"}}}}}}},"/api/voice/prompts/{key}/recording":{"put":{"tags":["voice"],"summary":"Record A Menu Line","description":"Play a person's own take of one menu line in place of Rooster's.\n\n⛔ONE LINE IN HIS VOICE PUTS TWO VOICES ON THE MENU. He objected to that\non 2026-09-10 (\"You have two different voices on the same menu\") when it\nhappened by accident. On purpose it is his call, so it is allowed, and the\npanel says so beside the button rather than refusing.","operationId":"record_a_menu_line_api_voice_prompts__key__recording_put","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_record_a_menu_line_api_voice_prompts__key__recording_put"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptRecordingOut"},"title":"Response Record A Menu Line Api Voice Prompts  Key  Recording Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["voice"],"summary":"Put Roosters Voice Back","description":"Drop a person's take of one line, and have Rooster record it again.","operationId":"put_roosters_voice_back_api_voice_prompts__key__recording_delete","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptRecordingOut"},"title":"Response Put Roosters Voice Back Api Voice Prompts  Key  Recording Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/callbacks/{ticket_id}/may-we-ring":{"get":{"tags":["voice"],"summary":"May We Ring This Callback","description":"The softphone's question before it dials a TICKET: is it inside the\ncustomer's calling hours right now, and if not, when?\n\n⛔WHY THIS EXISTS, 2026-09-24. Frederick: the out-of-hours pop-up applies\nto \"every ticket dial path that can refuse for hours ... the softphone\npath currently speaks the refusal and hangs up, so also show the pop-up in\nthe browser.\" `/outbound/from-browser` is a Plivo webhook: its answer goes\nto Plivo as XML, never to the browser, so the browser cannot learn from it\nwhy the call died. So the browser asks here first, and gets the SAME\nverdict -- `ringing_back.may_the_desk_ring`, the one function\n`/outbound/from-browser` and `/outbound/cell-bridge` both refuse with.\n\n⛔A READ, AND NOT THE GATE. The refusal at the dial stays exactly where it\nis: a screen's copy of the hour is as old as its last fetch, so\n`/outbound/from-browser` still speaks the refusal and hangs up if the\nhour turned between this answer and the call.","operationId":"may_we_ring_this_callback_api_voice_callbacks__ticket_id__may_we_ring_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}},{"name":"phone","in":"query","required":false,"schema":{"type":"string","default":"","title":"Phone"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallbackHoursResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/callbacks/{ticket_id}/ring":{"post":{"tags":["voice"],"summary":"Ring The Customer Back","description":"Ring the phone on this person's profile, then patch them to the customer.\n\nFor an ACCOUNT MANAGER, who has a cell phone or a landline. The consumer\ndesk has neither and dials from the browser instead -- see\n`outbound_from_the_browser` below.\n\n(Since 2026-09-24: for a US principal with a cell on their profile --\n`softphone_endpoints.outbound_channel_for`, the keypad's own rule. Anyone\nworking outside the US gets 421 and the dialpad; a blank country or a\nmissing cell gets 409 saying which field to fill in. Every answer this\nroute gives is one the ticket page prints -- G-60948.)","operationId":"ring_the_customer_back_api_voice_callbacks__ticket_id__ring_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RingBackResult"}}}},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutsideCallingHoursBody"}}},"description":"Conflict"},"421":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UseTheSoftphoneBody"}}},"description":"Misdirected Request"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/tickets/{ticket_id}/dial-check":{"post":{"tags":["voice"],"summary":"May We Dial This Number","description":"Asked by the softphone BEFORE it dials a number for a ticket (#3508).\n\nThe browser places its own leg, so the only refusal it could otherwise\nget is a spoken one on the call. This asks `customer_dial.\nnumber_to_dial` the same question the dial itself will ask, so a screen\ncan show the agent both numbers and the reason (the pop-up that does is\nnot built: it waits on a design, OPEN_ITEMS 2026-10-04; until then\n`apps/web/src/features/voice/voice.ts` -> `checkDialNumber` is its typed\nclient and nothing on a screen calls it). Reads only.","operationId":"may_we_dial_this_number_api_voice_tickets__ticket_id__dial_check_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DialNumberRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DialCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/tickets/{ticket_id}/dial-confirmations":{"post":{"tags":["voice"],"summary":"Confirm A Dial Number","description":"An agent confirms, on screen and against the transcript, that a number\nthis ticket's callers neither rang from nor were heard saying IS the\ncustomer's (#3508). Recorded with who and why\n(`customer_dial_confirmations`); never possible for a staff phone or one\nof our own lines. Dials nothing: the agent dials again afterwards.","operationId":"confirm_a_dial_number_api_voice_tickets__ticket_id__dial_confirmations_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DialConfirmationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DialCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/call-attempts":{"get":{"tags":["voice"],"summary":"Call Attempts","description":"How many real tries each of these tickets has had, and the next due time.\n\nFrederick, 2026-09-24: \"Make sure ... that we're tracking the number of\ncallback attempts.\" The count has been kept since 2026-09-17\n(`service.a_real_try`) and shown nowhere an agent works; this is what the\nticket page and the Calls queue read to say \"Attempt 2 of 3\" and \"Next\nattempt due ...\". A READ, and only a read -- it changes nothing, and it\nrefuses nothing: Frederick the same day, \"the system should always allow\nunlimited callbacks\", so a count past three is shown (\"Attempt 4\"), never\nenforced.","operationId":"call_attempts_api_voice_call_attempts_get","parameters":[{"name":"ticket_id","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","format":"uuid"},"title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CallAttemptsResponse"},"title":"Response Call Attempts Api Voice Call Attempts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/tickets/{ticket_id}/call":{"get":{"tags":["voice"],"summary":"Ticket Call","description":"The phone call behind a ticket: what was said, what was recorded, and\nthe number to ring back. `null` for a ticket that is not a call.\n\n⛔THE TICKET IS THE ONE SCREEN A PERSON WORKS FROM. Frederick, 2026-09-14:\n\"This should have created a ticket just like a email ticket that you can\nclick into, read the situation, and then there's a button there to do a\ncallback.\" Everything here already existed on `voice_calls`; nothing read\nit back from the ticket's side, so the desk sent people to an\norganization page instead, which answers none of it.\n\n⛔`call` STAYS THE INBOUND LEG, AND `other_calls` IS EVERYTHING ELSE.\nFrederick, 2026-09-21 (`WHAT_A_CALLBACK_TICKET_HOLDS.md`): \"the ticket\nrepresents both legs of the call ... and the agent's call out to resolve\nthe call. Their recordings summaries and transcripts.\" Before this,\n`service.call_for_ticket` returned only the customer's first inbound\ncall and every outbound attempt an agent placed to resolve the ticket --\neach with its own recording once `record_live_call` did its job -- was\ninvisible on the one screen a person works from. Neither list carries the\nfull transcript; see `_ticket_call_dict`.\n\n⛔`needs_disposition`, ADDED 2026-09-25 (#2186), so the ticket page can\noffer the disposition form for a call the softphone never tracked --\na cell-bridge callback (`ring_them_back`/`outbound/cell-bridge`) never\nfires the browser SDK events `TicketDetailPage`'s own\n`CallDispositionPrompt` trigger depends on for a softphone call. True\nfor a bridge call whose dial CONNECTED (`bridge_disposition.\ncall_ids_needing_disposition`, `dial_status = ringing_back.\nDIAL_ANSWERED` -- corrected 2026-09-25 by Fable's review, F2: a dial\nthat never reached the customer is never a candidate, the same\npopulation the softphone's own prompt never asks about either) with no\n`voice_call_dispositions` row yet; always `False` for the inbound leg\n(nobody dispositions the customer's own call) and for a call already\ndispositioned.","operationId":"ticket_call_api_voice_tickets__ticket_id__call_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"title":"Response Ticket Call Api Voice Tickets  Ticket Id  Call Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/calls/{call_uuid}/recording":{"get":{"tags":["voice"],"summary":"Call Recording","description":"The call's recording, as audio the ticket's player can play.\n\n⛔THE ONE WAY A RECORDING REACHES A SCREEN (ISS-1675). Mark, on the Desk:\n\"I'm unable to play or listen to the call recordings ... All the\nrecordings ... Open tickets and resolved tickets.\" The ticket used to put\n`recording_url` straight into `<audio src>`, and that is a Plivo URL our\naccount's basic auth guards, or a bucket link signed for an endpoint the\nAPI reaches and a browser may not. Read here, behind the same `cs:voice`\ngrant that shows the call at all, and served to a signed-in fetch\n(`fetchObjectUrl`), it plays whichever of the two the row holds.\n\n⛔HOLDS NO POOL CONNECTION WHILE THE AUDIO IS FETCHED (ISS-1761). The\nread goes to Plivo (up to 60 s) or the bucket; with `get_connection` the\nslot was held for all of it, p95 60 s and 2.9 s of pool wait for everyone\nelse. The row is one statement, so it takes a `BriefConnection`.","operationId":"call_recording_api_voice_calls__call_uuid__recording_get","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Call Recording Api Voice Calls  Call Uuid  Recording Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/bridge/{ticket_id}":{"post":{"tags":["voice"],"summary":"Outbound Bridge","description":"What the account manager hears when they pick up, and then the dial.\n\n⛔PUBLIC BY NECESSITY AND SIGNED BY DESIGN, like every other Plivo\ncallback here: Plivo has no session. Without a keypress this only ever\nSPEAKS; the customer is dialled only once somebody has pressed the key,\nso a voicemail box that answered cannot put a customer through to itself.","operationId":"outbound_bridge_api_voice_outbound_bridge__ticket_id__post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Outbound Bridge Api Voice Outbound Bridge  Ticket Id  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/bridge-done/{ticket_id}":{"post":{"tags":["voice"],"summary":"Outbound Bridge Done","description":"How the dial to the customer went -- the fact the callback policy runs on.\n\n⛔THIS IS THE ONLY PLACE WE EVER LEARN WHETHER A CALLBACK REACHED ANYBODY.\nPlivo posts `DialStatus` here when the `<Dial>` ends: `completed` if the\nfar end picked up, `busy`/`no-answer`/`timeout`/`cancel`/`failed` if it did\nnot. Until this existed the system recorded that we DIALLED and nothing\nabout whether it worked, so \"we try up to three times, then we close it\"\nhad no third fact to count and no first fact to stop on.\n\nFrederick, 2026-09-17: \"within that one business day, we can try them up\nto three times ... we make one more call back attempt after that we close\nthe call back attempt and you have to call us back to reset it.\"\n\n⛔PUBLIC BY NECESSITY AND SIGNED BY DESIGN, like every other Plivo callback\non this router: Plivo has no session, so the V3 HMAC is the authentication.\n\nThe XML hangs the account manager's leg up, which is what was going to\nhappen anyway -- the customer's call is over.","operationId":"outbound_bridge_done_api_voice_outbound_bridge_done__ticket_id__post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Outbound Bridge Done Api Voice Outbound Bridge Done  Ticket Id  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/cell-bridge":{"post":{"tags":["voice"],"summary":"Outbound Cell Bridge","description":"Ring this US principal's own cell, then bridge them to whatever they\ndialled on the keypad.\n\n⛔KEYED BY A CORRELATION ID THIS ROUTE MINTS, NOT BY TICKET. `outbound_\nbridge` (the ticket's own \"Ring them back\" button) finds its row by\nasking \"what is this ticket's most recent outbound row\" -- correct only\nbecause a ticket has one active callback at a time. A keypad dial may\ncarry no ticket at all, and even when it does the agent may type a\nnumber that is not the ticket's own -- so a fresh id goes in the answer\nURL itself, known before Plivo is ever asked, and\n`outbound_cell_bridge_answer`/`-done` below look the row up by exactly\nthat id. No ambiguity, whatever else is ringing.","operationId":"outbound_cell_bridge_api_voice_outbound_cell_bridge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CellBridgeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CellBridgeResult"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutsideCallingHoursBody"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/cell-bridge-answer/{correlation}":{"post":{"tags":["voice"],"summary":"Outbound Cell Bridge Answer","description":"What the staff member hears when their own cell picks up, then the\ndial to whoever the keypad rang -- the keypad's version of\n`outbound_bridge`.\n\n⛔PUBLIC BY NECESSITY AND SIGNED BY DESIGN, like every other Plivo\ncallback here: Plivo has no session. Without a keypress this only ever\nSPEAKS; the customer is dialled only once somebody has pressed the key.","operationId":"outbound_cell_bridge_answer_api_voice_outbound_cell_bridge_answer__correlation__post","parameters":[{"name":"correlation","in":"path","required":true,"schema":{"type":"string","title":"Correlation"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Outbound Cell Bridge Answer Api Voice Outbound Cell Bridge Answer  Correlation  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/cell-bridge-done/{correlation}":{"post":{"tags":["voice"],"summary":"Outbound Cell Bridge Done","description":"How the dial to whoever the keypad rang went -- the keypad's version\nof `outbound_bridge_done`. Settles the ticket's callback policy only when\nthis call carried a ticket; a raw dial has no policy to settle, only a\nresult to write. Also the done route for a softphone dial with no ticket\n(`outbound_from_browser`, ISS-1297), keyed by that leg's own CallUUID.","operationId":"outbound_cell_bridge_done_api_voice_outbound_cell_bridge_done__correlation__post","parameters":[{"name":"correlation","in":"path","required":true,"schema":{"type":"string","title":"Correlation"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Outbound Cell Bridge Done Api Voice Outbound Cell Bridge Done  Correlation  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/recording":{"post":{"tags":["voice"],"summary":"Outbound Recording","description":"Where Plivo hands back the recording of a callback.\n\nThe twin of `/inbound/recording`, and deliberately a SEPARATE route: that\none creates a voice_call row for a voicemail nobody had a row for yet.\nA callback already has its row, with its ticket on it, written before the\nfirst leg was even placed -- so this only ever fills the recording in.\nCreating a second row here would put a call on the board twice and detach\nthe recording from the ticket it belongs to.","operationId":"outbound_recording_api_voice_outbound_recording_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Outbound Recording Api Voice Outbound Recording Post"}}}}}}},"/api/voice/outbound/transcription":{"post":{"tags":["voice"],"summary":"Outbound Transcription","description":"Where Plivo hands back what was SAID on a callback.\n\n⛔EVERY CALL HAS TO BE READABLE, NOT JUST ROOSTER'S OWN. Frederick,\n2026-09-16: \"we should transcribe all calls so they'll calls can be read\nand analyzed by Rooster um, for training purposes and we can understand\nwhat calls are about if we want to see what all the calls are about for a\nmonth, analytics.\"\n\nRooster's own conversations already carry a transcript -- he is one end of\nthem, so `record_agent_transcript` writes what he heard and said. A\nCALLBACK has no Rooster on it: it is a manager and a customer, and until\nnow nothing was written down at all. A month of those was unreadable and\nunanalysable, which is exactly the gap above.\n\nThe twin of `/inbound/transcription`, and a separate route for the same\nreason `/outbound/recording` is: Plivo signs the URL it posts to, so each\npath verifies its own. Both delegate to the SAME\n`service.record_transcription` -- one way to write a transcript, two\ndoors it can arrive through.","operationId":"outbound_transcription_api_voice_outbound_transcription_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Outbound Transcription Api Voice Outbound Transcription Post"}}}}}}},"/api/voice/outbound/rooster/{rooster_call_id}":{"post":{"tags":["voice"],"summary":"Rooster Rings A Colleague","description":"Rooster rang somebody who works here. This is what they hear.\n\n⛔PUBLIC BY NECESSITY AND SIGNED BY DESIGN, like every other Plivo\ncallback in this module: Plivo has no session.\n\n⛔AND NOTHING IS SAID UNTIL A KEY IS PRESSED. Until then this only asks.\nA voicemail box, a pocket or a car answers a phone; none of them presses\none, and a nudge delivered to an answering machine while our records say\nwe reached them is worse than not ringing at all.","operationId":"rooster_rings_a_colleague_api_voice_outbound_rooster__rooster_call_id__post","parameters":[{"name":"rooster_call_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Rooster Call Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Rooster Rings A Colleague Api Voice Outbound Rooster  Rooster Call Id  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/outbound/rooster/{rooster_call_id}/missed":{"post":{"tags":["voice"],"summary":"Rooster Missed A Colleague","description":"Nobody pressed the key, so nobody heard it. Recorded as exactly that.\n\n⛔PUBLIC BY NECESSITY AND SIGNED BY DESIGN -- a Plivo callback.\n\nA `<Redirect>` rather than letting the leg simply end, because \"he rang\nand nobody was there\" is the fact that decides whether Slack should say\nso -- and a fall-through inside one XML document leaves no record at all.","operationId":"rooster_missed_a_colleague_api_voice_outbound_rooster__rooster_call_id__missed_post","parameters":[{"name":"rooster_call_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Rooster Call Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Rooster Missed A Colleague Api Voice Outbound Rooster  Rooster Call Id  Missed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/probe/hold":{"get":{"tags":["voice"],"summary":"Probe Hold","description":"What the SYNTHETIC CALLER's own leg does: nothing, for long enough.\n\n⛔THE PROBE NEEDS A CALLER WHO STAYS ON THE LINE. The first live run of\n`synthetic_call` used Plivo's public answer.xml, which ends immediately --\nthe call connected, was recorded, and lasted five seconds, so it proved\nthe carrier could reach us and nothing else. Holding the line is what lets\nthe greeting play, the options play, the tree fall through to Rooster, and\na transcript exist to judge.\n\n⛔PUBLIC AND UNSIGNED, DELIBERATELY, AND IT IS THE ONE ROUTE HERE THAT MAY\nBE. Every other voice webhook is signed because it acts on a real call --\nit books callbacks, reads accounts, opens tickets. This returns four\nfixed words of XML, reads nothing, writes nothing, and names nobody. A\nsignature here would gate the instrument on the very thing the instrument\nexists to check.","operationId":"probe_hold_api_voice_probe_hold_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Probe Hold Api Voice Probe Hold Get"}}}}}}},"/api/voice/outbound/from-browser":{"post":{"tags":["voice"],"summary":"Outbound From The Browser","description":"The softphone's outbound leg, so the customer sees OUR number.\n\n⛔THE CONSUMER DESK HAS NO PHONES. Frederick, 2026-09-14: \"Customer\nservice agents don't have [a cell phone or landline]. They have to talk a\nhundred percent through the computer.\" So their calls leave from the\nbrowser endpoint, and the only place the caller id can be set on that leg\nis here, in the XML Plivo fetches for it.\n\n⛔THIS IS THE ANSWER URL FOR THE SOFTPHONE'S PLIVO APPLICATION, and until\nthat application points here the browser presents whatever it presents.\nPointing it here is a Plivo change, not a deploy.","operationId":"outbound_from_the_browser_api_voice_outbound_from_browser_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Outbound From The Browser Api Voice Outbound From Browser Post"}}}}}}},"/api/voice/inbound/dropped":{"post":{"tags":["voice"],"summary":"Inbound Dropped","description":"Where a caller lands when the stream dies under them.\n\n⛔THE CALLER IS STILL ON THE LINE, AND UNTIL THIS THEY GOT SILENCE. Plivo\nplays this because `<Stream keepCallAlive=\"true\">` leaves the leg up when\nthe websocket goes; before it existed there was nothing after the stream\nelement, so the call simply ended. Frederick, 2026-09-14, after staging\nredeployed mid-sentence: \"Rooster said hi, and the phone hung up on me.\"\n\nThe same floor the agent's own absence gets: apologise, and take a\nrecording either way. `inbound_answer`'s failsafe branch carries the\nreasoning for doing BOTH. Whether a callback gets booked, and on what\nnumber, is `_how_to_book_a_dropped_call`'s job -- see its own comment\nfor the current rule (RULE 4) and the three it replaced, kept rather\nthan deleted. ⛔RULE 4 BOOKS NOTHING IN PRACTICE TODAY -- that is the\nintended behaviour, not a gap; see `_how_to_book_a_dropped_call`'s own\ndocstring.\n\n⛔AND THIS CALL'S OWN ROW IS ALWAYS SETTLED, whatever gets booked or\nnot, via `service.settle_after_dropped_stream` -- a row left reading\n'ringing' blocks a redial on its ticket for up to thirty minutes (see\nthat function's own docstring for the production numbers). Frederick:\n\"they should just be able to call them back ... it's easier than\nsending the human back through the loop.\"","operationId":"inbound_dropped_api_voice_inbound_dropped_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Dropped Api Voice Inbound Dropped Post"}}}}}}},"/api/voice/phone-health":{"get":{"tags":["voice"],"summary":"Phone Health Today","description":"Is the phone up, and what has it done today.\n\nFrederick, 2026-09-14: \"Customer service analytics page should show\nanalytics on phone calls coming in for the day ... how many phone calls\nhas rooster answered? How many callbacks have been scheduled? How many\ncallbacks have been completed? We should be able to see the health of the\nphone system.\"","operationId":"phone_health_today_api_voice_phone_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneHealthOut"}}}}}}},"/api/voice/calls/{call_uuid}/answer-here":{"post":{"tags":["voice"],"summary":"Answer Here","description":"Accept a call a colleague is transferring to YOU, on this softphone.\n\n⛔IT IS NO LONGER A PICKUP BUTTON, AND THAT IS THE ISSUE (#1768). Frederick,\n2026-09-19: \"No, we don't take live calls. So there's no reason for a call\nto be broadcasted. All calls are callback tickets. However, the only time\nthat bar should show up is on your page when someone's trying to transfer\nyou a call.\" So this accepts a DIRECTED transfer -- `handoff.\ntransfer_call_to`'s browser branch, taken for a colleague with no American\nnumber to ring -- and refuses everything else.\n\n⛔THE OLD GUARD'S PURPOSE SURVIVES; ITS TEST COULD NOT. It refused anything\nthat was not \"inbound and still ringing\", to stop `redirect_live_call`\nmoving a call belonging to somebody else's conversation. Right instinct,\nand that risk is unchanged. But a caller mid-conversation with Rooster is\n`in-progress`, not `'ringing'` -- `ON_HOLD_STATUS` is literally the string\n`'ringing'`, which every answered inbound call starts at -- so keeping the\nold test would have refused every directed transfer there is. The\nreplacement is strictly stronger: inbound, still live, AND directed at\nTHIS principal with nobody having taken it.\n\n⛔THE CLAIM HAPPENS BEFORE THE REDIRECT. The old order redirected first and\nupdated the row afterwards, so two people pressing Answer in the same\nsecond both moved the same caller. The conditional UPDATE is now the\nreferee, and a failed redirect gives the claim back rather than leaving a\ncall nobody can ever take.","operationId":"answer_here_api_voice_calls__call_uuid__answer_here_post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerHereResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/to-the-desk/{call_uuid}":{"post":{"tags":["voice"],"summary":"Inbound To The Desk","description":"The XML Plivo fetches once somebody has clicked Answer.\n\nPublic and signed, like every other Plivo callback here: Plivo has no\nsession. The signature is verified over THIS path before a parameter is\nread.","operationId":"inbound_to_the_desk_api_voice_inbound_to_the_desk__call_uuid__post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound To The Desk Api Voice Inbound To The Desk  Call Uuid  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/voice/inbound/desk-done/{call_uuid}":{"post":{"tags":["voice"],"summary":"Inbound Desk Done","description":"What happens when the desk does NOT pick up.\n\n⛔THE CALLER IS STILL ON THE LINE AT THIS POINT. Plivo fetches this when\nthe dial ends for any reason, and by far the most likely reason is that\nnobody is registered -- a closed tab answers in milliseconds. Returning\nnothing would leave a person holding a dead leg after being told somebody\nwas coming, which is worse than never having offered.\n\nSo they go back to the floor: the failsafe voicemail, which takes a\nmessage and books the callback.","operationId":"inbound_desk_done_api_voice_inbound_desk_done__call_uuid__post","parameters":[{"name":"call_uuid","in":"path","required":true,"schema":{"type":"string","title":"Call Uuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Inbound Desk Done Api Voice Inbound Desk Done  Call Uuid  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/clock":{"get":{"tags":["rooster"],"summary":"Get Clock","description":"The current time, read fresh off Postgres on every call -- never this\nprocess's clock (app/platform/clock.py's module docstring explains why:\nthe same process-vs-Postgres skew this codebase has already lost rows\nto). Carries the business timezone (`settings.escalation_tz`) alongside\nthe absolute UTC timestamp so Rooster can speak to a Pacific-time team\nwithout doing zone conversion himself.","operationId":"get_clock_api_rooster_clock_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Clock Api Rooster Clock Get"}}}}}}},"/api/rooster/feedback":{"post":{"tags":["rooster"],"summary":"Record Feedback","operationId":"record_feedback_api_rooster_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordFeedbackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Feedback Api Rooster Feedback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/post-send-signal":{"post":{"tags":["rooster"],"summary":"Record Post Send Signal","description":"What a reviewer's approval could not see: the customer said it made\nno sense, an agent had to walk something back, or it escalated into a\nbigger issue (docs/SUPERVISION_DESK.md \"Post-send signals\"). Gated on\nthe SAME `rooster:feedback:write` grant as `POST /feedback` -- recording\na post-send observation is the same class of human judgement, just\narriving later.","operationId":"record_post_send_signal_api_rooster_post_send_signal_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordPostSendSignalRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Post Send Signal Api Rooster Post Send Signal Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/rate":{"get":{"tags":["rooster"],"summary":"Get Rate","operationId":"get_rate_api_rooster_skills__skill_key__rate_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Window Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Rate Api Rooster Skills  Skill Key  Rate Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/gate":{"get":{"tags":["rooster"],"summary":"Get Gate","operationId":"get_gate_api_rooster_skills__skill_key__gate_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Gate Api Rooster Skills  Skill Key  Gate Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["rooster"],"summary":"Set Gate","operationId":"set_gate_api_rooster_skills__skill_key__gate_patch","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetGateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Gate Api Rooster Skills  Skill Key  Gate Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/status":{"get":{"tags":["rooster"],"summary":"Get Status","operationId":"get_status_api_rooster_skills__skill_key__status_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Status Api Rooster Skills  Skill Key  Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/decision":{"get":{"tags":["rooster"],"summary":"Get Decision","description":"A read-only preview of `evaluate_graduation` -- computes but does NOT\napply any transition (that only ever happens via `record_disposition` or\nthe explicit POST .../evaluate below).","operationId":"get_decision_api_rooster_skills__skill_key__decision_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Window Days"}},{"name":"min_sample","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":20,"title":"Min Sample"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Decision Api Rooster Skills  Skill Key  Decision Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/spot-check":{"get":{"tags":["rooster"],"summary":"Get Spot Check Sample","operationId":"get_spot_check_sample_api_rooster_skills__skill_key__spot_check_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"sample_size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":5,"title":"Sample Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Spot Check Sample Api Rooster Skills  Skill Key  Spot Check Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/evaluate":{"post":{"tags":["rooster"],"summary":"Evaluate Skill","description":"Forces a re-check and applies whatever transition it implies -- the\nauto-demote-on-drift hook, callable on demand (there is no scheduler in\nthis foundational wave).","operationId":"evaluate_skill_api_rooster_skills__skill_key__evaluate_post","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Window Days"}},{"name":"min_sample","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":20,"title":"Min Sample"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Evaluate Skill Api Rooster Skills  Skill Key  Evaluate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/verticals/{vertical}/outcome-rate":{"get":{"tags":["rooster"],"summary":"Get Vertical Outcome Rate","operationId":"get_vertical_outcome_rate_api_rooster_skills__skill_key__verticals__vertical__outcome_rate_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"vertical","in":"path","required":true,"schema":{"enum":["b2c","b2g"],"type":"string","title":"Vertical"}},{"name":"window_judgements","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":200,"title":"Window Judgements"}},{"name":"harness","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Harness"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Vertical Outcome Rate Api Rooster Skills  Skill Key  Verticals  Vertical  Outcome Rate Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/verticals/{vertical}/status":{"get":{"tags":["rooster"],"summary":"Get Vertical Status","operationId":"get_vertical_status_api_rooster_skills__skill_key__verticals__vertical__status_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"vertical","in":"path","required":true,"schema":{"enum":["b2c","b2g"],"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Vertical Status Api Rooster Skills  Skill Key  Verticals  Vertical  Status Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/verticals/{vertical}/graduation":{"get":{"tags":["rooster"],"summary":"Get Vertical Graduation","description":"A read-only preview of `evaluate_vertical_graduation` -- computes but\ndoes NOT apply any transition (that only ever happens via\n`record_post_send_signal` or the explicit POST .../evaluate below).","operationId":"get_vertical_graduation_api_rooster_skills__skill_key__verticals__vertical__graduation_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"vertical","in":"path","required":true,"schema":{"enum":["b2c","b2g"],"type":"string","title":"Vertical"}},{"name":"window_judgements","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":200,"title":"Window Judgements"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Vertical Graduation Api Rooster Skills  Skill Key  Verticals  Vertical  Graduation Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/verticals/{vertical}/evaluate":{"post":{"tags":["rooster"],"summary":"Evaluate Vertical Skill","description":"Forces a re-check of THIS (skill_key, vertical) pair and applies\nwhatever transition it implies -- promote on the Wilson lower bound\nclearing 95%, revoke on the observed rate falling below 90%, no-op\ntouch otherwise (docs/SUPERVISION_DESK.md's deliberate hysteresis).","operationId":"evaluate_vertical_skill_api_rooster_skills__skill_key__verticals__vertical__evaluate_post","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"vertical","in":"path","required":true,"schema":{"enum":["b2c","b2g"],"type":"string","title":"Vertical"}},{"name":"window_judgements","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":200,"title":"Window Judgements"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Evaluate Vertical Skill Api Rooster Skills  Skill Key  Verticals  Vertical  Evaluate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/skills/{skill_key}/graduation-summary":{"get":{"tags":["rooster"],"summary":"Get Graduation Summary","description":"THE single read for the desk and the owner view (docs/\nSUPERVISION_DESK.md \"Expose the numbers\"): both verticals for one\nskill_key, side by side, each carrying the observed rate, the Wilson\nlower bound, how many judgements are in the window, distance to the\ngraduation bar, and the current graduated/not state -- everything\nneeded to render \"consumer: 54.8% observed, 42.5% we can be sure of, 62\njudged, 52.5 points to go\" without a second call per vertical.","operationId":"get_graduation_summary_api_rooster_skills__skill_key__graduation_summary_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"window_judgements","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":200,"title":"Window Judgements"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Graduation Summary Api Rooster Skills  Skill Key  Graduation Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/pending":{"get":{"tags":["rooster"],"summary":"Pending Cs Tickets","description":"Tickets waiting on Rooster's CS-drafting skill, oldest first --\ndrained by the same local worker (tools/rooster_worker/worker.py) that\nalready reads /issues/intake/pending and /playbooks/author/pending, in\nthe SAME `{\"conversations\": [{\"id\": ...}, ...]}` shape that worker's\n`fetch_pending` already expects (naming kept consistent across surfaces\nrather than inventing a ticket-specific key for this one).\n\nRegistered BEFORE `GET /cs/tickets/{ticket_id}/context` below, same\nreason /issues/intake/pending precedes /issues/intake/{conversation_id}/\ncontext: a literal path segment has to be matched before a `{ticket_id}`\none gets a chance to fail UUID validation on the word \"pending\".\n\nSee `agent_draft.tickets_awaiting_draft`'s own docstring for the rule\nthis encodes (once per inbound customer message, never re-fighting a\nhuman who just discarded a draft) and gated on the SAME `rooster:cs:draft`\ngrant as every other CS-drafting route -- reading which tickets need a\nturn is part of the same drafting capability, not a new one.","operationId":"pending_cs_tickets_api_rooster_cs_tickets_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Cs Tickets Api Rooster Cs Tickets Pending Get"}}}}}}},"/api/rooster/industry/pending":{"get":{"tags":["rooster"],"summary":"Pending Industry Orgs","description":"Organizations waiting on Rooster's industry-map desk, worst gap first.\n\nSame `{\"conversations\": [{\"id\": ...}, ...]}` shape every other pending\nsurface uses (`/cs/tickets/pending`, `/issues/intake/pending`,\n`/playbooks/author/pending`) so `tools/rooster_worker/worker.py`'s\nexisting `fetch_pending` drains it with no new client code -- naming kept\nconsistent across surfaces rather than inventing an industry-specific key.\n\nTHE QUEUE IS ALREADY BUILT AND ALREADY RANKED. This is a thin read over\n`enrichment_service.select_tier_a_adp_queue`, which orders by\n`domain.rank_adp_queue`: the segment furthest below the 90 percent ADP\ntarget first, then the oldest attempt, then the organization missing MORE\nsurrounding data. Nothing new decides priority here; a second ranking would be\na second truth.\n\nWHY THE TIER A SLICE IS THE RIGHT QUEUE FOR A DESK, and not a limitation:\nAUTONOMOUS_FACILITY_ENRICHMENT.md S5's cost ladder puts these at the floor\n-- an org whose website resolves to `.gov`/`.us` \"need[s] NO search and NO\nLLM\" for its cheapest facts. Handing the desk the cheapest work first is\nwhat keeps the model seat free for the judgment cases and for everything\nelse Rooster does; see `tools/rooster_worker/desks.py`'s industry block for\nthat argument in full.\n\nRead-only, and deliberately so. Claiming, writing and promoting a fact all\nhappen through the enrichment service's own promotion rules\n(`enrichment_service.promote_org_field`), which enforce source tier and\nnever overwrite a human-set field -- a desk that could write around those\nrules would be a second, weaker copy of them.","operationId":"pending_industry_orgs_api_rooster_industry_pending_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pending Industry Orgs Api Rooster Industry Pending Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/ar/pending":{"get":{"tags":["rooster"],"summary":"Pending Ar Collections","description":"Organizations whose automated dunning cadence has run out of\nautomated steps and still carry a balance -- the AR/collections hat\n(Frederick, 2026-08-14, `docs/01_decisions/ONE_ROOSTER_MANY_SKILLS.md`).\n\nSame `{\"conversations\": [{\"id\": ...}, ...]}` shape every other pending\nsurface uses, so `tools/rooster_worker/worker.py`'s existing\n`fetch_pending` drains it with no new client code. Read-only: deciding\nwhether to escalate collections happens by filing a proposal through\nthe supervision channel (`POST /api/rooster/supervision/report`), never\nby this route or any write this skill makes.","operationId":"pending_ar_collections_api_rooster_ar_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Ar Collections Api Rooster Ar Pending Get"}}}}}}},"/api/rooster/ar/context/{org_id}":{"get":{"tags":["rooster"],"summary":"Get Ar Collections Context","description":"Everything Rooster needs to judge one account's AR standing --\naging, dunning history and recent CS tickets. See\n`ar_collections_context.build_ar_collections_context`'s own docstring\nfor the full read and its boundary.","operationId":"get_ar_collections_context_api_rooster_ar_context__org_id__get","parameters":[{"name":"org_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Ar Collections Context Api Rooster Ar Context  Org Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/outreach/referrals/pending":{"get":{"tags":["rooster"],"summary":"Pending Outreach Referrals","description":"Referrals waiting on Rooster's content-creation/outbound skill --\nthe same hat named in `docs/01_decisions/ONE_ROOSTER_MANY_SKILLS.md`\n(\"content creation and outbound messaging\").\n\nBacked by `ladder_service.list_pending_referrals_for_rooster` -- the\nSAME table `GET /api/sales/referrals/pending` already reads for a\nhuman, reshaped into the `{\"conversations\": [{\"id\": ...}, ...]}`\ncontract `tools/rooster_worker/worker.py`'s `fetch_pending` expects,\nMINUS any referral Rooster already filed a proposal for -- see that\nfunction's own docstring for why (without it, the same referral\nre-drafts every ~10s poll tick forever). Rooster may draft the next\nmessage to the referred contact (`rooster-outreach` skill) and file it\nas a proposal; it may never call `confirm_referral` itself -- a\nmachine-parsed name is not acted on until a human agrees\n(`ladder_service.record_referral`'s own docstring).","operationId":"pending_outreach_referrals_api_rooster_outreach_referrals_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Outreach Referrals Api Rooster Outreach Referrals Pending Get"}}}}}}},"/api/rooster/outreach/referrals/{referral_id}":{"get":{"tags":["rooster"],"summary":"Get Outreach Referral","description":"One pending referral's own detail -- who named whom, at which\naccount, and the sentence it came out of. `fetch_pending` only ever\nreads `id` off the queue list above; this is where the skill's task\nprompt gets the rest (`org_id`/`from_contact_id`) so it can call\n`get_outreach_context`. Filtered in Python over the same, deliberately\nsmall, pending list -- there is never more than a handful of open\nreferrals at once (`list_pending_referrals`'s own module: \"the queue\nthat makes the ladder move\").","operationId":"get_outreach_referral_api_rooster_outreach_referrals__referral_id__get","parameters":[{"name":"referral_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Referral Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Outreach Referral Api Rooster Outreach Referrals  Referral Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/context":{"get":{"tags":["rooster"],"summary":"Get Cs Ticket Context","description":"One-call read bundle for an autonomous agent (a Goose recipe driving\nRooster's brain over MCP, see tools/rooster_mcp/) to read a ticket\nITSELF, in place of this module calling the model provider server-side.\nGated on the SAME rooster:cs:draft grant as draft_cs_reply above -- no\nnew function key for the agent's own read of a ticket it already has\npermission to draft for.","operationId":"get_cs_ticket_context_api_rooster_cs_tickets__ticket_id__context_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Cs Ticket Context Api Rooster Cs Tickets  Ticket Id  Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cases/{case_id}/research":{"post":{"tags":["rooster"],"summary":"Open Case Research Desk","description":"\"Discuss\" on a case card opens this room (docs/01_decisions/\nROOSTER_RESEARCHES_DISCREPANCIES.md unit 4) -- the same working-out-loud\nspace a ticket's \"Research this ticket\" button already opens\n(`research_desk.open_desk`), anchored to a case instead (migration 0354,\n`research_desk.open_case_desk`).\n\n⛔THIS IS \"SAME ROOM FOR EVERYONE\", not \"expand in place\"\n(docs/*work-surfaces-navigate-never-expand-in-place*): the web navigates\nto a dedicated page for this room rather than growing the card, exactly\nthe discipline every other worklist on Today already follows.","operationId":"open_case_research_desk_api_rooster_cases__case_id__research_post","parameters":[{"name":"case_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Case Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Case Research Desk Api Rooster Cases  Case Id  Research Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cases/{case_id}/research/say":{"post":{"tags":["rooster"],"summary":"Say In Case Research Desk","description":"The human's turn in a case's room. Same invariant as the ticket room:\nnothing said here reaches a customer -- and a case-anchored room has no\nticket to draft into in the first place, so `record_draft`'s whole\n\"lands in the ordinary reply box\" escape hatch does not even apply here.","operationId":"say_in_case_research_desk_api_rooster_cases__case_id__research_say_post","parameters":[{"name":"case_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Case Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaseResearchSayRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Say In Case Research Desk Api Rooster Cases  Case Id  Research Say Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/research/pending":{"get":{"tags":["rooster"],"summary":"Pending Research Desks","description":"Research rooms where a human spoke last and is waiting on Rooster.\n\nSame `{\"conversations\": [{\"id\": ...}]}` shape as every other Rooster\nqueue, drained by the same worker. Registered before the `{desk_id}`\nroutes below for the same reason `/cs/tickets/pending` is: a literal\nsegment has to match before a UUID parser gets to fail on the word.","operationId":"pending_research_desks_api_rooster_cs_research_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Research Desks Api Rooster Cs Research Pending Get"}}}}}}},"/api/rooster/cs/research/{desk_id}/look-up":{"get":{"tags":["rooster"],"summary":"Look Up From Research Desk","description":"Look anything up mid-conversation: a code, a name, a number, a box.\n\n⛔THE DOSSIER ANSWERS THE FIRST QUESTION, NOT THE SECOND. It resolves the\ncodes quoted in the ticket, which is what an opening brief needs. Then the\naccount manager asks \"what about the rest of his mail this year\", or\n\"which box did that one ship in\", or \"has this facility had this before\" --\nand a fixed bundle has nothing to say.\n\nFrederick, 2026-09-03, on what this whole surface is meant to feel like:\n\"I want the same experience as me giving you a screenshot and then you\nsearching the data and pulling up the records and seeing the details and\nexplaining it and going through it.\"\n\nThat experience is not a report. It is being able to LOOK, repeatedly,\nwhile the conversation goes on. This is that.\n\nOne field, the same one the parcel-search screen uses: a parcel code, an\ninmate name or number, a facility, or a box tracking number all work, and\nthe server decides which it is rather than the caller guessing.\n\nReturns only what a customer may be told -- code, status, recipient,\nfacility, contents, tracking, and the station timeline. No technician\nname, no internal note.","operationId":"look_up_from_research_desk_api_rooster_cs_research__desk_id__look_up_get","parameters":[{"name":"desk_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desk Id"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":200,"default":"","title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Look Up From Research Desk Api Rooster Cs Research  Desk Id  Look Up Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/research/{desk_id}/prior-ticket/{number}":{"get":{"tags":["rooster"],"summary":"Read Prior Ticket From Research Desk","description":"WHAT WE ACTUALLY SAID TO THIS CUSTOMER BEFORE.\n\n⛔THE DOSSIER SHOWS THAT PRIOR TICKETS EXIST, NOT WHAT IS IN THEM. It can\nsay #59669 was closed without a reply; it cannot say what she asked, or\nwhat we told her the time before that. Answering a customer without\nreading what we last promised them is how a support reply contradicts its\nown account -- and on a thread already in front of an Ombudsman, the\ncontradiction is the story.\n\nFrederick, 2026-09-03: \"he's supposed to reason over maybe other related\ntickets to make sure we're doing a complete response, anything we've said\nto that customer in the past. That's supposed to be a full picture.\"\n\n⛔SAME ORGANIZATION ONLY. Drafting for one customer must never become a\nwindow onto another's correspondence; the room's own ticket decides whose\nhistory is readable, which is why the desk id is in the path.\n\nPrivate notes come back marked rather than hidden -- an internal note is\noften the only record of why a ticket was closed, and #59669 was closed on\none reading \"Canceled account\" -- but they are flagged so nothing internal\nis ever mistaken for something the customer was told.","operationId":"read_prior_ticket_from_research_desk_api_rooster_cs_research__desk_id__prior_ticket__number__get","parameters":[{"name":"desk_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desk Id"}},{"name":"number","in":"path","required":true,"schema":{"type":"integer","title":"Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read Prior Ticket From Research Desk Api Rooster Cs Research  Desk Id  Prior Ticket  Number  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/research/{desk_id}":{"get":{"tags":["rooster"],"summary":"Get Research Desk","description":"The room: the conversation so far and the dossier it is built on.","operationId":"get_research_desk_api_rooster_cs_research__desk_id__get","parameters":[{"name":"desk_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desk Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Research Desk Api Rooster Cs Research  Desk Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/research/{desk_id}/reply":{"post":{"tags":["rooster"],"summary":"Reply In Research Desk","description":"Rooster's turn in the room.\n\n⛔THIS IS NOT A REPLY TO THE CUSTOMER and no path from here reaches one.\nIt is Rooster talking to the account manager about what he found. When\nthey have settled on an approach the human asks for a draft, and THAT\ngoes to `/draft` below.","operationId":"reply_in_research_desk_api_rooster_cs_research__desk_id__reply_post","parameters":[{"name":"desk_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desk Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchReplyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reply In Research Desk Api Rooster Cs Research  Desk Id  Reply Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/research/{desk_id}/draft":{"post":{"tags":["rooster"],"summary":"Draft From Research Desk","description":"Write the reply they agreed on into the ticket's normal reply box.\n\n⛔ONLY WHEN THE HUMAN HAS ASKED. Frederick, 2026-09-02: \"once the research\nis done ... rooster can ask ... if he wants to formulate a[response]. The\nresponse goes into the ticket response. The human can then do as normal,\nedit it, and then hit send.\" Rooster must not reach this on his own\ninitiative at the end of a research turn -- the point of the room is that\nthe human decides what is said.\n\nIt lands as an ordinary `state='draft'` agent message, so it appears in\nthe composer the account manager already uses, editable and unsent. This\nendpoint sends nothing.","operationId":"draft_from_research_desk_api_rooster_cs_research__desk_id__draft_post","parameters":[{"name":"desk_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desk Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchDraftRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Draft From Research Desk Api Rooster Cs Research  Desk Id  Draft Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/research":{"get":{"tags":["rooster"],"summary":"Get Cs Ticket Research","description":"Every operational fact behind one ticket, gathered before a human asks.\n\nSee `ticket_research` for the incident this exists because of. In short:\n`build_ticket_context` returns the conversation and the account's ticket\nhistory and NOTHING operational, so on a ticket naming four parcel codes\nRooster could only restate the question. This is the other half.\n\nDeterministic on purpose -- the queries are fixed and run every time, so\nthe agent never has to know which lookup to reach for and can never\nsilently skip one. Its judgment goes on TOP of this, in the note it writes\nfor the account manager; the gathering is not a judgment call.\n\nSame `rooster:cs:draft` grant as every other CS route: reading the facts\nbehind a ticket he may already draft for needs no new key. Scoped to this\nticket's own organization for the account history, so drafting for one\ncustomer never becomes a window onto another's.","operationId":"get_cs_ticket_research_api_rooster_cs_tickets__ticket_id__research_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Cs Ticket Research Api Rooster Cs Tickets  Ticket Id  Research Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/organizations/memory":{"get":{"tags":["rooster"],"summary":"Get Organization Memory","description":"Customer memory for one organization, given directly (`org_id`) or by\na ticket that belongs to it (`ticket_id`) -- Frederick: \"he should be\nable to look at a customer's history... has this customer had this\ncomplaint in the past.\" This is what makes that history reachable from a\nRooster surface that has no ticket in hand at all (issue intake, the CS\ndirective, playbook authoring, the direction line), not only the\nCS-draft recipe that already reads it inside `get_cs_ticket_context`\nabove.\n\nGated on DIRECTION_WRITE, the same \"this caller is a Rooster agent, not a\nhuman UI\" grant `get_clock` and the issue-intake/playbook-authoring reads\nalready reuse below -- this route's own scoping (`disclosure.for_customer`/\n`graph_registry.resolve_customer_graphs`, inside\n`agent_draft.build_organization_memory`) is what keeps every response\ncustomer-safe and sealed to exactly one org, never which grant a caller\nhappens to hold.\n\nExactly one of `org_id`/`ticket_id` is expected; if `ticket_id` is\nsupplied it resolves to THAT ticket's own org, and `query` defaults to\nthat ticket's own subject + latest customer message when left blank. If\nneither resolves to a real organization -- no id given, an unknown\nticket, a ticket with no org (B2C), or an unknown org -- this returns an\nempty bundle rather than a 404: \"if a tool cannot resolve an org, it\nreturns nothing rather than searching broadly,\" never an error an agent\nmight retry into a wider search.","operationId":"get_organization_memory_api_rooster_cs_organizations_memory_get","parameters":[{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}},{"name":"ticket_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},{"name":"query","in":"query","required":false,"schema":{"type":"string","default":"","title":"Query"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Organization Memory Api Rooster Cs Organizations Memory Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/memory/jobs":{"get":{"tags":["rooster"],"summary":"Get Job Memory","description":"#2303 (R-038, Frederick 2026-08-18): \"Rooster shouldn't decide what to\nremember. He should just know what bank to read for the job he's doing,\nbecause Rooster does many jobs.\" This route IS that wire: pass the\ncalling job's own `skill_key` (the SAME constant `departments.\ndepartment_for` already resolves -- e.g. `domain.CS_REPLY_SKILL_KEY`,\n`departments.TECHNICAL_TRIAGE_SKILL_KEY`) and it reads every knowledge\nbank THAT job's own department declares\n(`departments.DEPARTMENT_PROFILES[dept].knowledge_graphs`) -- never a\nbank the caller picks by hand, and never fewer than the department\ndeclares. A department that declares more than one bank (Technical:\ncompany decisions, software root causes, AND equipment fixes -- three\ngraphs, since Zep searches one per call) is read across all of them and\nmerged, each fact tagged with which bank it came from\n(`job_memory.read_job_banks`).\n\nGated on the same `rooster:today:direction` grant `get_organization_\nmemory` above already reuses for exactly the same reason: this is a\ncross-surface Rooster-agent read, not a human UI page.\n\nRaises 404 for a `skill_key` this build does not recognize\n(`departments.UnknownSkill`) -- fails closed rather than guessing the\nwidest department. `org_id` is honored only for a department whose own\nprofile reaches org graphs (today: CS-Gov); every other department\nignores it, exactly as `departments.graphs_for_department` documents.\n`configured=False` in the response means Zep itself is unconfigured\n(`ZEP_API_KEY` unset) -- an environment fact, not an error.","operationId":"get_job_memory_api_rooster_memory_jobs_get","parameters":[{"name":"skill_key","in":"query","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"query","in":"query","required":true,"schema":{"type":"string","title":"Query"}},{"name":"org_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Job Memory Api Rooster Memory Jobs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/agent-draft":{"post":{"tags":["rooster"],"summary":"Write Cs Agent Draft","description":"Dumb persist: writes `body.text`, composed entirely by the calling\nagent, as an unsent `ticket_messages` row. Calls NO model -- see\napp/modules/rooster/agent_draft.py. Idempotent the same way\ndraft_cs_reply is: if this ticket already carries an unsent agent draft,\nthat draft is returned as-is (`created: false`) and no second row is\never stacked.","operationId":"write_cs_agent_draft_api_rooster_cs_tickets__ticket_id__agent_draft_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDraftRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Write Cs Agent Draft Api Rooster Cs Tickets  Ticket Id  Agent Draft Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/escalate":{"post":{"tags":["rooster"],"summary":"Escalate Cs Ticket","description":"Rooster's escalation tool (see app/modules/rooster/escalation.py):\nhe lacks the information, the authority, or a tool to answer this\nticket himself, and is saying so rather than guessing. Records a\n'proper_escalation' disposition -- docs/SUPERVISION_DESK.md: a SUCCESS,\nnot a failure -- and raises a human-visible ActionItem routed the same\nway every other ticket-scoped card in this codebase already is\n(app/modules/customer_service/ticket_routing.py): the ticket's own\nassignee, else (b2g) the agency's account manager, else a visible pool.\nNever an account manager seeing a consumer ticket or vice versa.\n\nGated on the SAME `rooster:cs:draft` grant as every other CS-drafting\nroute -- deciding he cannot draft this one himself is part of the same\ndrafting capability, not a new one. Idempotent per ticket: a repeat call\nwhile an escalation for this ticket is still on file reuses that card\nand records no second disposition (`created: false`).","operationId":"escalate_cs_ticket_api_rooster_cs_tickets__ticket_id__escalate_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EscalateTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Escalate Cs Ticket Api Rooster Cs Tickets  Ticket Id  Escalate Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/triage":{"post":{"tags":["rooster"],"summary":"Triage Cs Ticket","description":"Rooster's triage tool (see app/modules/rooster/triage.py): reaches\none of three conclusions on a ticket BEFORE ever drafting anything --\n'reply' (proceed to draft exactly as before this route existed, no side\neffect), 'suggest_spam' (unsolicited marketing/bulk mail -- do not\ndraft; a human confirms via the pre-existing `POST .../spam` button),\nor 'unsure' (genuinely ambiguous -- leave it for a human rather than\nguess either way). Records a 'declined' disposition for the latter two\n-- docs/SUPERVISION_DESK.md's outcome taxonomy, a SUCCESS, never\n'abandoned' -- and raises a human-visible ActionItem routed the same\nway every other ticket-scoped card in this codebase already is.\n\nGated on the SAME `rooster:cs:draft` grant as every other CS-drafting\nroute -- deciding whether a ticket deserves a reply is part of the same\ndrafting capability, not a new one. Idempotent per ticket, exactly like\n`POST .../escalate`: a repeat call while a triage card for this ticket\nis still on file reuses that card and records no second disposition\n(`created: false`).","operationId":"triage_cs_ticket_api_rooster_cs_tickets__ticket_id__triage_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriageTicketRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Triage Cs Ticket Api Rooster Cs Tickets  Ticket Id  Triage Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/trigger-make-good":{"post":{"tags":["rooster"],"summary":"Trigger Cs Make Good","description":"Rooster's make-good trigger tool (master spec §3.5: \"Agent job:\nconverse, resolve, trigger, escalate\"; see\napp/modules/rooster/make_good_trigger.py): fires this customer's tier-1\nmake-good event for the current billing month (deterministic, capped\none per customer per billing month by make_good_events' own UNIQUE\nindex -- never a second cap invented here) and drafts the apology it\npromises, unsent, for a human to review and release exactly like any\nother agent draft (migration 0629's releaser floor covers it the same\nway).\n\nGated on the SAME `rooster:cs:draft` grant as every other CS-drafting\nroute -- deciding a service breach deserves an apology is part of the\nsame drafting capability, not a new one, and is NOT one of the four\nConstitution-gated launch acts (customer_service/agent_acts.py) --\nthose are a human (Frederick) flipping a switch per act; this is\nbaseline conversational judgement, the same posture escalate/triage\nalready have.","operationId":"trigger_cs_make_good_api_rooster_cs_tickets__ticket_id__trigger_make_good_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerMakeGoodRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Trigger Cs Make Good Api Rooster Cs Tickets  Ticket Id  Trigger Make Good Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/tickets/{ticket_id}/draft-attempt-failed":{"post":{"tags":["rooster"],"summary":"Record Cs Draft Attempt Failed","description":"`tools/rooster_worker/worker.py` calls this after a Goose turn on a\nCS-draft ticket produces no draft -- a timeout, a non-zero `goose run`\nexit, or a clean exit that left the ticket still on\n`GET /cs/tickets/pending` (he read it and correctly wrote nothing).\nNEVER called on a successful turn.\n\nThis is the durable, cross-restart half of the fix for a live production\nbug: the worker's own in-memory poison tracking (`WorkerState`) dies\nevery time the `rooster` Railway service redeploys, so a ticket he\ncannot answer (e.g. marketing mail with no repliable text) sat\npermanently at the front of the oldest-first queue and re-cost three\nGoose runs on every restart before any real customer was reached. See\n`agent_draft.record_draft_attempt_failed` (the write) and\n`agent_draft.tickets_awaiting_draft` (the read this changes) for the\nfull contract, in particular why no separate \"un-decline\" endpoint\nexists: a new inbound customer message re-arms the ticket on its own.\n\nGated on the SAME `rooster:cs:draft` grant as every other CS-drafting\nroute -- recording that a turn was attempted is part of the same\ndrafting capability, not a new one. This route can only ever suppress\nre-offering a ticket to Rooster; it has no power to mark a ticket spam\nor closed, which stays a human (Frederick) decision made elsewhere.","operationId":"record_cs_draft_attempt_failed_api_rooster_cs_tickets__ticket_id__draft_attempt_failed_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DraftAttemptFailedRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Cs Draft Attempt Failed Api Rooster Cs Tickets  Ticket Id  Draft Attempt Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/internal/tickets/{ticket_id}/context":{"get":{"tags":["rooster"],"summary":"Get Internal Ticket Context","operationId":"get_internal_ticket_context_api_rooster_internal_tickets__ticket_id__context_get","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ticket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Internal Ticket Context Api Rooster Internal Tickets  Ticket Id  Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/disclosure/policies":{"get":{"tags":["rooster"],"summary":"List Disclosure Policies","operationId":"list_disclosure_policies_api_rooster_disclosure_policies_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Disclosure Policies Api Rooster Disclosure Policies Get"}}}}}}},"/api/rooster/disclosure/policies/{audience}":{"patch":{"tags":["rooster"],"summary":"Set Disclosure Policy","operationId":"set_disclosure_policy_api_rooster_disclosure_policies__audience__patch","parameters":[{"name":"audience","in":"path","required":true,"schema":{"type":"string","title":"Audience"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDisclosurePolicyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Disclosure Policy Api Rooster Disclosure Policies  Audience  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/chronicle":{"get":{"tags":["rooster"],"summary":"Get Chronicle","description":"The Company Chronicle (`chronicle.build_chronicle`), wired to a live\nroute for the first time (W4) -- a rolling 7-day (`chronicle.\nDEFAULT_WINDOW_DAYS`), internal-audience brief of recent Zep graph\nactivity: what became true, what stopped being true, what was committed\nto. Scope is this principal's own internal Zep scope\n(`graph_registry.resolve_principal_graphs`, no org grant widened in --\ncompany-wide knowledge graphs only, never another caller's org data).\n\nDegrade-safe by construction, not by any extra handling here: an empty\nscope or an unconfigured Zep account (`ZEP_API_KEY` unset, the stub\ngateway) makes `build_chronicle` return a well-formed EMPTY chronicle\nrather than raise -- see that function's own docstring.","operationId":"get_chronicle_api_rooster_chronicle_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Chronicle Api Rooster Chronicle Get"}}}}}}},"/api/rooster/live-ops":{"get":{"tags":["rooster"],"summary":"Get Live Ops","description":"The 24-business-hour live-ops window (`live_ops.read_live_ops_window`),\nwired to a live route for the first time (W4) -- \"so Rooster keeps\nFriday's context on Monday\" (live_ops.py's own docstring): recent ticket\nactivity plus notable outbox events, full-tenant (no org narrowing --\nthis principal's own internal scope, `live_ops.for_principal`).\n\nDegrade-safe by construction: a non-internal scope or no matching\nactivity returns a well-formed EMPTY window (still carrying accurate\n`window_start`/`window_end` bounds) rather than raise -- see\n`read_live_ops_window`'s own docstring.","operationId":"get_live_ops_api_rooster_live_ops_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Live Ops Api Rooster Live Ops Get"}}}}}}},"/api/rooster/activity":{"get":{"tags":["rooster"],"summary":"Get Rooster Activity","description":"Company OS 2026-09-30 handoff item 6, System -> Rooster's Activity\ntab (`design/Rooster.dc.html`, CHANGES.md §7): the status strip plus\n\"Actions taken on his own\". See `app.modules.rooster.activity`'s own\nmodule docstring for exactly what each field honestly measures and the\none design state (a genuine process-down \"Not running\") this build\ncannot yet tell apart from \"stalled\".\n\n\"Research cases handed to a person\" and \"Questions closed unanswered\"\nare NOT in this response: the first is `GET /api/rooster/cases/handed`\n(a `cases/` module read, kept in that module rather than reached into\nfrom here); the second has no backing data source in this build yet\n(see the PR body) -- the frontend renders its own empty state for it\nrather than this route inventing one.","operationId":"get_rooster_activity_api_rooster_activity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Rooster Activity Api Rooster Activity Get"}}}}}}},"/api/rooster/today/direction/context":{"get":{"tags":["rooster"],"summary":"Get Direction Context","description":"The read half: exactly the day's context the server-side composer\nwould have sent a model (open action items by band, live clocks),\nplus the date the line is for so the agent never has to guess at the\nserver's clock.","operationId":"get_direction_context_api_rooster_today_direction_context_get","parameters":[{"name":"principal_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Direction Context Api Rooster Today Direction Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/today/direction/agent-line":{"post":{"tags":["rooster"],"summary":"Write Direction Line","description":"The write half: a dumb persist of the line the AGENT composed, upserted\nonto direction_lines' existing (principal_id, for_date) key. Calls NO\nmodel. Blank text is refused (422) rather than persisted -- a blank line\nrenders as an empty headline on Today with no hint anything failed.","operationId":"write_direction_line_api_rooster_today_direction_agent_line_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDirectionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Write Direction Line Api Rooster Today Direction Agent Line Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/directive/context":{"get":{"tags":["rooster"],"summary":"Get Cs Directive Context","description":"Everything true about the CS queue right now, computed not guessed:\nthe aggregate counts (the same function the live page uses, so the two\ncan never disagree), every open ticket with its clock, SLA state, owner,\nwhether the customer is waiting on us and whether a draft already\nexists, and the contract/receivables position of the orgs involved.","operationId":"get_cs_directive_context_api_rooster_cs_directive_context_get","parameters":[{"name":"kinds","in":"query","required":false,"schema":{"type":"string","default":"b2c,b2g","title":"Kinds"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Cs Directive Context Api Rooster Cs Directive Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cs/directive/agent-line":{"post":{"tags":["rooster"],"summary":"Write Cs Directive","description":"Dumb persist of the directive the AGENT composed. Calls NO model.\nBlank text is refused: an empty header on the CS page reads as \"nothing\nneeds you\", which is a dangerous thing to say by accident.","operationId":"write_cs_directive_api_rooster_cs_directive_agent_line_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentDirectiveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Write Cs Directive Api Rooster Cs Directive Agent Line Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/intake/pending":{"get":{"tags":["rooster"],"summary":"Pending Issue Intakes","description":"Bug conversations waiting on Rooster, oldest first. Drained by the\nlocal worker: derived from the conversation itself, so a run that dies\nmid-turn leaves the work pending rather than stranded.","operationId":"pending_issue_intakes_api_rooster_issues_intake_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Issue Intakes Api Rooster Issues Intake Pending Get"}}}}}}},"/api/rooster/issues/intake/{conversation_id}/context":{"get":{"tags":["rooster"],"summary":"Get Issue Intake Context","description":"What the reporter has said, the draft so far, the vocabulary an issue\nmust be filed into, and everything already open on the board.","operationId":"get_issue_intake_context_api_rooster_issues_intake__conversation_id__context_get","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Issue Intake Context Api Rooster Issues Intake  Conversation Id  Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/intake/{conversation_id}/images":{"get":{"tags":["rooster"],"summary":"Get Issue Intake Images","description":"The screenshots themselves, base64, so Rooster can LOOK at them.\n\nHe used to be told he could not see inside an attachment, and said so to\nreporters -- asking people to describe a picture he was already holding.\nSeparate from the context endpoint because image bytes are large and\nevery other caller of that endpoint wants text. Kept for whatever else\nreads the raw attachments (e.g. an eventual UI); the vision pass itself\nreads through `describe_issue_intake_images` below, which fetches these\nsame bytes server-side rather than round-tripping them through a caller.","operationId":"get_issue_intake_images_api_rooster_issues_intake__conversation_id__images_get","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Issue Intake Images Api Rooster Issues Intake  Conversation Id  Images Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/intake/{conversation_id}/describe-images":{"post":{"tags":["rooster"],"summary":"Describe Issue Intake Images","description":"Actually look at what the reporter attached, and say what is in it.\n\nISS-570. `tools/rooster_worker/worker.py` is a synchronous poll loop\nthat talks to this API over httpx and holds no database connection of\nits own -- `app.core.model_client`'s credential resolution needs one\n(vault first, per CLAUDE.md's \"AI model registry\"), so the vision call\nhas to happen HERE, in the API process, not in the worker. Same gate as\nthe sibling `GET /images` above: this is a Rooster-agent-only route, not\na UI surface.\n\n`describe_screenshot_attachments` (`app/modules/issues/intake_service.py`)\ndoes the actual work: fetch the screenshots, call `Surface.\nBUG_INTAKE_VISION` through the registry, return the description or None.\nNever raises past a missing conversation -- a failed vision pass is not\na 500, it is `{\"description\": None}`, so the worker's own contract\n(\"a screenshot we could not read must leave Rooster saying so, never\ninventing\") holds all the way through this route.","operationId":"describe_issue_intake_images_api_rooster_issues_intake__conversation_id__describe_images_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Describe Issue Intake Images Api Rooster Issues Intake  Conversation Id  Describe Images Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/intake/{conversation_id}/respond":{"post":{"tags":["rooster"],"summary":"Respond To Issue Intake","description":"Dumb persist of Rooster's turn. Calls NO model.\n\n`trace_steps` are shown to the reporter as what Rooster actually did\nwhile they talked, so they must be real tool calls, never illustrative\nones. A turn with no draft is a question, which is the normal first\nturn.\n\nPasses Rooster's own Slack notifier (`_slack_notifier`, below -- NOT\nDamien's bot, see that function's own incident writeup) so\n`issues_intake.respond` can DM the reporter that this turn landed. Same\nseam every other Rooster-lifecycle DM in this file already goes through\n(build-question, merged, build-failed, staged).","operationId":"respond_to_issue_intake_api_rooster_issues_intake__conversation_id__respond_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueTurnRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Respond To Issue Intake Api Rooster Issues Intake  Conversation Id  Respond Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/intake/{conversation_id}/attempt-failed":{"post":{"tags":["rooster"],"summary":"Record Issue Intake Attempt Failed","description":"`tools/rooster_worker/worker.py` calls this after a Goose turn on a\nbug-intake conversation produces no reply -- a timeout, a non-zero\n`goose run` exit, a missing `goose` binary, or a clean exit that left\nthe conversation still on `GET /issues/intake/pending`. NEVER called on\na successful turn.\n\nThe durable, cross-restart half of the harness fix for a live\nproduction incident: `WorkerState`, the worker's own in-memory\ntracking, dies every time the `rooster` service redeploys. See\n`issues_intake.record_intake_attempt_failed` (the write -- past\n`MAX_INTAKE_ATTEMPTS` it tells the reporter honestly over Slack and\nfiles or bumps a REAL, self-diagnosed bug through the same pipeline\nthat already fixes a customer's report unaided, never removing the\nconversation from `issues_intake.pending_for_rooster`) for the full\ncontract.\n\n`principal` -- the calling agent's own authenticated identity (\"Rooster\nPipeline\" for the real worker) -- is passed through as\n`filer_principal_id`: a self-filed defect is authored by the system that\ndiagnosed it, never by the reporter whose bug report merely happened to\ntrip it.\n\nGated on the SAME `DIRECTION_WRITE` grant as every other agent-facing\nintake route -- recording that a turn was attempted is part of the same\ncapability as taking one.","operationId":"record_issue_intake_attempt_failed_api_rooster_issues_intake__conversation_id__attempt_failed_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeAttemptFailedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Issue Intake Attempt Failed Api Rooster Issues Intake  Conversation Id  Attempt Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/playbooks/author/pending":{"get":{"tags":["rooster"],"summary":"Pending Playbook Authoring","description":"Playbook conversations waiting on Rooster, oldest first.","operationId":"pending_playbook_authoring_api_rooster_playbooks_author_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Playbook Authoring Api Rooster Playbooks Author Pending Get"}}}}}}},"/api/rooster/playbooks/author/{conversation_id}/context":{"get":{"tags":["rooster"],"summary":"Get Playbook Authoring Context","description":"The conversation, the draft so far, and the playbook it is FOR. A\nplaybook written without its own segment and stage is generic advice.","operationId":"get_playbook_authoring_context_api_rooster_playbooks_author__conversation_id__context_get","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Playbook Authoring Context Api Rooster Playbooks Author  Conversation Id  Context Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/playbooks/author/{conversation_id}/respond":{"post":{"tags":["rooster"],"summary":"Respond To Playbook Authoring","description":"Dumb persist of Rooster's turn. Calls NO model.","operationId":"respond_to_playbook_authoring_api_rooster_playbooks_author__conversation_id__respond_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaybookTurnRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Respond To Playbook Authoring Api Rooster Playbooks Author  Conversation Id  Respond Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/brain":{"post":{"tags":["rooster"],"summary":"Report Agent Brain","description":"Rooster says which brain he came up on.\n\nFrederick, 2026-08-05, looking at the System page: \"It says [claude-sonnet]\nfor reasoning. It should be using Kimi K3... and before that it might even\nhave been on ChatGPT. So this is not even being updated.\"\n\nHe was right, and worse than stale: that panel read a hardcoded default\nwhile /api/health reported anthropic as a STUB, so it printed a model name\nfor surfaces calling nothing, and Rooster's real brain appeared nowhere.\nThis is the observed value -- written at boot by the process that resolved\nit, stamped with when -- so a panel can report what ran instead of what\nsomeone configured.","operationId":"report_agent_brain_api_rooster_brain_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBrainReport"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Report Agent Brain Api Rooster Brain Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/seat/probe-result":{"post":{"tags":["rooster"],"summary":"Report Seat Probe Result","description":"The container reports whether one of its Claude Code seats actually\nworks, right after proving it (or failing to).\n\nFrederick, 2026-09-15, after pasting the wrong value into the\n`claude_code_seat` box twice: \"Don't make me do this again.\" Production\nthe same day: the vault accepted the bad paste, the container needed a\nmanual restart to read it, and the only way anyone learned it was wrong\nwas an agent reading container logs -- `claude-code| Failed to\nauthenticate. API Error: 401 Invalid bearer token`. This route is the\nother half of closing that: the System page's Claude Code card\n(`ClaudeCodeSeatPanel.tsx`) reads `last_probe_error`/`last_probe_detail`\noff `GET /api/system/credentials` -- the SAME columns/route every other\ncredential's probe verdict already surfaces through (`system.service.\nrecord_credential_probe_result`) -- so this never invents a second way\nto show a verdict.\n\nRecorded against WHICHEVER credential the probe actually ran against\n(`body.credential`) -- `claude_code_seat` (Seat A, unchanged: the card\nis about whether Rooster's dedicated issues engineer works, not about\nwhich of ITS two credentials happened to carry it this pass -- see\n`SeatProbeResult.auth`) or `builder_seat` (Seat B, RA-45, new). An\nabsent or unrecognised value falls back to `claude_code_seat` rather\nthan raising -- a malformed report must never 500, and every call site\nthat predates this field meant Seat A anyway.","operationId":"report_seat_probe_result_api_rooster_seat_probe_result_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatProbeResultReport"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Report Seat Probe Result Api Rooster Seat Probe Result Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/builder/credential":{"post":{"tags":["rooster"],"summary":"Report Builder Credential","description":"What the daily credential probe found (rooster-credential-check.yml),\nand -- the only caller actually present in this repository as of this\nbuild -- `tools/rooster_worker/drain.py`'s own `seat_is_alive`\npreflight: the build seat itself, checked BEFORE anything is claimed.\n\nPresence is not validity: the probe makes a real authenticated call,\nbecause /api/health once reported the Anthropic key live over a revoked\none. A failure raises one action item naming the secret to replace; a\nlater success clears it.\n\nTHIS IS ALSO WHERE THE WHOLE QUEUE LEARNS THE SEAT IS DOWN. Frederick,\n2026-08-11, on the Mac-hosted build seat sleeping overnight: \"the issue\njust stays filed... the team knows the developer has the flu; the work\nis not lost and nobody is guessing.\" `seat_is_alive`'s own preflight\nalready refuses to CLAIM anything while the seat is down (protecting\n`build_attempts`), but on its own that is invisible on any individual\nissue's own record. A failed probe here stamps every issue already\nqueued with `build_unreachable_since`/`_detail`\n(`mark_build_queue_unreachable`) so a person looking at any one of them\nsees \"queued, seat offline since 9pm\" instead of a row that simply\nnever moves; a healthy probe clears it (`clear_build_queue_unreachable`)\nthe moment the seat answers again. Best-effort: a failure marking/\nclearing the queue must not turn a real credential result into a 500 --\nthe credential card above is still the authoritative signal either way.","operationId":"report_builder_credential_api_rooster_builder_credential_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuilderCredentialReport"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Report Builder Credential Api Rooster Builder Credential Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/work-queue":{"get":{"tags":["rooster"],"summary":"Issue Work Queue","description":"What Rooster is actually allowed to start work on, worst first.\n\nA bug needs no permission to fix. A feature appears here only once the\nfounder has approved building it, so this endpoint is itself the gate:\nan unapproved feature is simply not in the list, and there is nothing for\nthe agent to decide.","operationId":"issue_work_queue_api_rooster_issues_work_queue_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Issue Work Queue Api Rooster Issues Work Queue Get"}}}}}}},"/api/rooster/issues/claim-for-build":{"post":{"tags":["rooster"],"summary":"Claim Issue For Build","description":"Hand the next queued issue to a builder, and mark it as being built.\n\nThe claim and the status change are one statement, so two builders can\nnever take the same issue and the board shows \"being built\" from the moment\nwork starts rather than when it finishes. Returns `{\"issue\": null}` on an\nempty queue -- not a 404, because nothing is wrong: an idle drain is the\ncorrect state when there is nothing to drain.\n\n`lane` (query param, defaults to 'code' -- every caller that predates\nthe two-seat split keeps its exact previous behaviour) is NEVER trusted\non its own. `issues.service.claim_next_for_build` checks, before\ntouching a single row, that `principal` holds the grant NAMED for this\nexact lane (`domain.BUILD_CLAIM_FUNCTION_KEYS`) -- Seat A's own\nprincipal holds `rooster:build:claim:code` only and Seat B's own holds\n`rooster:build:claim:work_order` only, so a mismatched `lane` is\nrefused with 403 regardless of `DIRECTION_WRITE`'s own broader grant.\nSee `LaneNotPermittedForPrincipal`'s own docstring for the incident this\nreplaces (an independent bug-hunt review proved, with a repro, that\nbefore this check existed either seat's own token could claim from the\nother's lane).","operationId":"claim_issue_for_build_api_rooster_issues_claim_for_build_post","parameters":[{"name":"lane","in":"query","required":false,"schema":{"type":"string","default":"code","title":"Lane"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Claim Issue For Build Api Rooster Issues Claim For Build Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/draft-reviews/claim":{"post":{"tags":["rooster"],"summary":"Claim Draft Review","description":"Claim-first per pull request head sha (2026-09-28,\n`docs/01_decisions/rulings/2026-09-28-go-twice-as-fast.md`): the\ndrain's draft-review pass calls this BEFORE spending a `claude -p`\ncall, and reviews the pull request only when `claimed` comes back\ntrue. Reused permission dependency (`DIRECTION_WRITE`), same as the\nneighboring `/issues/claim-for-build` above -- both are the same\nmachine principal (`rooster-pipeline@pigeon.ly`) doing autonomous\ndrain work, never a person.","operationId":"claim_draft_review_api_rooster_draft_reviews_claim_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimDraftReviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Claim Draft Review Api Rooster Draft Reviews Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/draft-reviews/{head_sha}/posted":{"post":{"tags":["rooster"],"summary":"Mark Draft Review Posted","description":"Called only after `gh pr comment` itself succeeds -- claim, THEN\nact, THEN flip (CLAUDE.md, \"the state-integrity guard\").","operationId":"mark_draft_review_posted_api_rooster_draft_reviews__head_sha__posted_post","parameters":[{"name":"head_sha","in":"path","required":true,"schema":{"type":"string","title":"Head Sha"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mark Draft Review Posted Api Rooster Draft Reviews  Head Sha  Posted Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/pr-reviews/seat":{"get":{"tags":["rooster"],"summary":"Pr Review Seat","operationId":"pr_review_seat_api_rooster_pr_reviews_seat_get","parameters":[{"name":"pr_number","in":"query","required":true,"schema":{"type":"integer","title":"Pr Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pr Review Seat Api Rooster Pr Reviews Seat Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/pr-reviews/latest":{"get":{"tags":["rooster"],"summary":"Latest Pr Review","operationId":"latest_pr_review_api_rooster_pr_reviews_latest_get","parameters":[{"name":"pr_number","in":"query","required":true,"schema":{"type":"integer","title":"Pr Number"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Latest Pr Review Api Rooster Pr Reviews Latest Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/pr-reviews/claim":{"post":{"tags":["rooster"],"summary":"Claim Pr Review","description":"Claim-first per pull request head sha, same shape as\n`/draft-reviews/claim` above: the pre-merge review lane calls this\nBEFORE spending a `claude -p` call, and reviews the pull request only\nwhen `claimed` comes back true.","operationId":"claim_pr_review_api_rooster_pr_reviews_claim_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimPrReviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Claim Pr Review Api Rooster Pr Reviews Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/pr-reviews/{head_sha}/verdict":{"post":{"tags":["rooster"],"summary":"Record Pr Review Verdict","description":"Called only after the `review/bugs` commit status itself has been\nposted to GitHub -- claim, THEN review, THEN act, THEN flip.","operationId":"record_pr_review_verdict_api_rooster_pr_reviews__head_sha__verdict_post","parameters":[{"name":"head_sha","in":"path","required":true,"schema":{"type":"string","title":"Head Sha"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordPrReviewVerdictRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Pr Review Verdict Api Rooster Pr Reviews  Head Sha  Verdict Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/pr-reviews/override":{"post":{"tags":["rooster"],"summary":"Record Pr Review Override","description":"A human's `review-override` label is always logged -- see\n`pr_review.record_override`'s own docstring for why this is safe to\ncall more than once for the same head.","operationId":"record_pr_review_override_api_rooster_pr_reviews_override_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordPrReviewOverrideRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Pr Review Override Api Rooster Pr Reviews Override Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/release-stalled":{"post":{"tags":["rooster"],"summary":"Release Stalled Issue Builds","description":"Return dead builds to the queue; hand over the ones that keep dying.\n\nCalled by the drain loop on every pass. A build that claimed an issue and\nvanished otherwise leaves it reading \"being built now\" forever -- which is\nexactly how ISS-20 and ISS-22 came to sit unreachable, looking like work in\nprogress.","operationId":"release_stalled_issue_builds_api_rooster_issues_release_stalled_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Release Stalled Issue Builds Api Rooster Issues Release Stalled Post"}}}}}}},"/api/rooster/issues/merged-unproven":{"get":{"tags":["rooster"],"summary":"Merged Unproven Issues","description":"Fixes already merged to main that nobody has yet proven reached\nstaging -- what `tools/rooster_worker/drain.py`'s `verify_merged_pass`\npolls every drain pass, oldest first, and retries with ONLY its deploy\nmachinery (no model, no build seat). See\n`issues_service.list_merged_unproven`/`record_verify_failed` for the\nself-repair contract this feeds.","operationId":"merged_unproven_issues_api_rooster_issues_merged_unproven_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Merged Unproven Issues Api Rooster Issues Merged Unproven Get"}}}}}}},"/api/rooster/issues/promotion-readiness":{"get":{"tags":["rooster"],"summary":"Issues Promotion Readiness","description":"May production be promoted right now, and if not, what is holding it.\n\nCalled by `tools/rooster_worker/drain.py`'s `promote_pass` before it\ntouches Railway. The decision lives here, next to the statuses it reads,\nrather than in the worker: the worker knows how to deploy, not who is\nowed a verification.\n\n`may_auto_promote` is the whole answer. `blocked_by` exists so a hold is\nnever silent -- fifteen fixes once parked at `fix_staged` and the board\nlooked healthy, which is the failure this endpoint is part of removing.","operationId":"issues_promotion_readiness_api_rooster_issues_promotion_readiness_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Issues Promotion Readiness Api Rooster Issues Promotion Readiness Get"}}}}}}},"/api/rooster/issues/promotion-hold":{"put":{"tags":["rooster"],"summary":"Record Promotion Hold","description":"The promoter says why it is not advancing production, or that it is.\n\nCalled by `tools/rooster_worker/drain.py`'s `promote_pass` -- the one\nprocess that can see main's history and so the only thing that knows\nwhether a hold is a designed gate (somebody's own commit ahead of\nRooster's fixes; a human-reported fix nobody has verified) or a promotion\nthat simply did not happen. `audit.checks.check_promotion_lag` reads\nthis (`settings_service.PROMOTION_HOLD`) and relays the promoter's own\nreason instead of guessing \"waiting only on the script\" -- ISS-585 is\nthe build task that guess produced, handed to a builder who cannot\npromote anything.","operationId":"record_promotion_hold_api_rooster_issues_promotion_hold_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromotionHoldRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Promotion Hold Api Rooster Issues Promotion Hold Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/verify-failed":{"post":{"tags":["rooster"],"summary":"Record Issues Verify Failed","description":"One shared deploy/verify retry failed to prove every listed\nmerged-but-unproven issue reached staging. Paced and, past\n`issues_service.MAX_VERIFY_ATTEMPTS` consecutive failures, escalated to\na self-filed, buildable defect -- never a person's card, unless the\nfailure names a genuinely external cause (an expired credential, an\naccount out of credit), the one exception\n`issues_service.record_verify_failed` still raises a card for.","operationId":"record_issues_verify_failed_api_rooster_issues_verify_failed_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyFailedRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Issues Verify Failed Api Rooster Issues Verify Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/fix-rejected":{"post":{"tags":["rooster"],"summary":"Record Reporter Rejected Fix","description":"The reporter told Rooster, in the conversation, that the fix on\nstaging does not work.\n\nISS-374. The \"No, still broken\" button on the issue page already\nrequeues (#965); this is the same answer arriving in the other place\nreporters actually say it -- the \"Answer Rooster\" box on that same page.\nWithout it a filed conversation is talk-only by construction, so every\n\"it's not fixed\" produced another sentence and moved nothing.\n\nRooster's own turn decides THAT a reply is a rejection -- that is\njudgment about a person's words and belongs to the agent. What the\nrejection DOES is `issues_service.record_reporter_rejected_fix`, which\nis the button's own path, so the two can never mean different things.","operationId":"record_reporter_rejected_fix_api_rooster_issues__issue_id__fix_rejected_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReporterRejectedFixRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Reporter Rejected Fix Api Rooster Issues  Issue Id  Fix Rejected Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-question":{"post":{"tags":["rooster"],"summary":"Record Issue Build Question","description":"The builder needs one thing from a person before it can continue.\n\nNOT a failure, and deliberately does not burn an attempt: a builder blocked\non \"which back button did you press\" has not failed at anything. The issue\nleaves the queue until somebody answers, so the drain moves on to the next\none rather than stalling.","operationId":"record_issue_build_question_api_rooster_issues__issue_id__build_question_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildQuestionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Issue Build Question Api Rooster Issues  Issue Id  Build Question Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-upstream":{"post":{"tags":["rooster"],"summary":"Record Issue Build Upstream","description":"The builder read the code and the cause is in one of Marc's services.\n\n`tools/rooster_worker/drain.py`'s `FILE-UPSTREAM:` marker lands here.\nNOT a failure and NOT a pull request: the issue leaves the build queue,\nis tagged for the named service, and is filed to that project on Marc's\nGitLab through the same `issues_service.file_upstream` a self-filed\nfinding uses -- so Marc is told, and the reporter sees \"waiting on\nMarc\", by machinery that already exists. See\n`issues_service.record_build_upstream` for why this door is needed\n(ISS-541) and what it deliberately does not charge.\n\n422 for a service key the registry does not know: the drain charges\nthat as an ordinary failed attempt so the next builder is told the\nexact name that was wrong.","operationId":"record_issue_build_upstream_api_rooster_issues__issue_id__build_upstream_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildUpstreamRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Issue Build Upstream Api Rooster Issues  Issue Id  Build Upstream Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-needs-decision":{"post":{"tags":["rooster"],"summary":"Record Issue Build Needs Decision","description":"The builder read the code and the fix changes a rule Frederick set on\npurpose. `tools/rooster_worker/drain.py`'s `NEEDS-DECISION:` marker\nlands here: the issue becomes a feature request waiting on his build\napproval, and no attempt is spent (ISS-1268; see\n`issues_service.record_build_needs_decision`).","operationId":"record_issue_build_needs_decision_api_rooster_issues__issue_id__build_needs_decision_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildNeedsDecisionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Issue Build Needs Decision Api Rooster Issues  Issue Id  Build Needs Decision Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/merged":{"post":{"tags":["rooster"],"summary":"Record Merged Issue Fix","description":"The fix is merged to main, but not yet proven on staging.\n\nRecorded the moment the merge succeeds, because the merge is the point of no\nreturn: from there the change is on main whatever happens next. ISS-24\nmerged and deployed, then a later step failed, and with nothing remembering\nthe merge the loop rebuilt an already-merged fix twice.\n\nDeliberately no status change -- `fix_staged` still means one thing only:\nsomebody has confirmed staging is serving this commit.","operationId":"record_merged_issue_fix_api_rooster_issues__issue_id__merged_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergedFixRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Merged Issue Fix Api Rooster Issues  Issue Id  Merged Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-failed":{"post":{"tags":["rooster"],"summary":"Record Failed Issue Build","description":"A builder took this and produced nothing. Put it back now.\n\nWaiting for the stall timeout would leave an issue whose build failed in\nninety seconds reading \"being built now\" for the next hour. Past three\nattempts this escalates to Needs You instead of looping.","operationId":"record_failed_issue_build_api_rooster_issues__issue_id__build_failed_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedBuildRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Failed Issue Build Api Rooster Issues  Issue Id  Build Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-harness-failed":{"post":{"tags":["rooster"],"summary":"Record Harness Failed Issue Build","description":"`tools/rooster_worker/drain.py` calls this instead of `/build-failed`\nwhen the build's own machinery broke -- a timeout, a checkout that never\nstarted, CI that never reported, a merge or crash that has nothing to do\nwith the fix -- rather than a build that ran, reasoned, and produced a\nwrong or failing change. See `issues_service.record_harness_failure` for\nthe full contract: this does NOT spend a build attempt, and past a\nbounded remediation ladder it self-files a real defect against Rooster's\nown harness instead of ever landing \"needs a person\" on someone's Today\nfor a fix nobody actually tried.\n\n`principal` is passed through as BOTH `actor_principal_id` and\n`filer_principal_id` -- the calling agent's own authenticated identity\nis who touched the row and who a self-filed defect is authored by,\nexactly the same reuse `record_issue_intake_attempt_failed` above makes\nfor the identical reason: never the reporter, who did not diagnose this.","operationId":"record_harness_failed_issue_build_api_rooster_issues__issue_id__build_harness_failed_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HarnessFailedBuildRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Harness Failed Issue Build Api Rooster Issues  Issue Id  Build Harness Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-heartbeat":{"post":{"tags":["rooster"],"summary":"Record Issue Build Heartbeat","description":"`tools/rooster_worker/drain.py` calls this every minute for every\nclaim it is holding, from the moment it takes one until the outcome is\nrecorded -- through the build, the CI wait, the merge and the deploy.\n\nIt is the word `issues_service.release_stalled_builds` measures silence\nfrom. Without it that reaper had only the claim's own age, and a build\nstill inside its CI wait is routinely older than the 55-minute window\nwhile working perfectly -- so it was requeued mid-flight and the\nmachinery was blamed for it (ISS-329). See\n`issues_service.record_build_heartbeat` for the full contract: it\nstamps one column and changes nothing else, spends no attempt, and\nfiles nothing.\n\nNEVER 404s, DELIBERATELY, and takes no body. A heartbeat is the thing\nthat must not be able to end a build: an issue somebody else has\nalready moved (or one this stamp cannot find at all) comes back\n`held: false`, which is the drain's cue that the work it is doing is no\nlonger claimed by it -- not an error to raise at a loop whose only\nother option is to die holding the claim.","operationId":"record_issue_build_heartbeat_api_rooster_issues__issue_id__build_heartbeat_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Issue Build Heartbeat Api Rooster Issues  Issue Id  Build Heartbeat Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-model":{"post":{"tags":["rooster"],"summary":"Record Model","description":"`tools/rooster_worker/drain.py` calls this once per build attempt,\njust before it starts the builder subprocess, to say which model that\nattempt runs on. Frederick, 2026-10-06: builders run on the cheapest\nmodel that builds and escalate to a stronger one after a failed\nattempt -- so the split has to be visible afterwards (`GET\n/api/system/seat-usage`'s `builds_by_model_last_7_days`), or \"did Sonnet\nland it or did it take an Opus retry\" is unanswerable.\n\nBookkeeping, never a gate: it stamps the open `seat_build_sessions` row\n(`seat_usage.record_model`), spends no attempt, moves no status, and\nNEVER 404s -- an issue with no open session (a human re-claim, a\nsession already closed) comes back `recorded: false`, which the drain\nignores. Ruling:\n`docs/01_decisions/rulings/2026-10-06-cheap-builders-strong-reviewers.md`.","operationId":"record_model_api_rooster_issues__issue_id__build_model_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildModelRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Model Api Rooster Issues  Issue Id  Build Model Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/seat-calls":{"post":{"tags":["rooster"],"summary":"Record Seat Call","description":"`tools/rooster_worker/claude_cli.py` posts every `claude -p` run it\nmakes for a lane whose server files no row of its own (bug intake, fix\nreview, board seat, the seat probe), so `model_calls` says what the Claude\nseat spent, per lane. Frederick, 2026-10-06: \"make sure it's built to be\ntoken efficient and not burn through the weekly allocation again.\"\n\nThe rooster container has no database, so the worker cannot write the row\nitself (`app.modules.rooster.seat_calls`); a 2xx here is the receipt the\nworker counts on. 422 for a lane or provider this will not file (a lane\nwhose server already records it would be counted twice). Bookkeeping only,\nnever a gate: it moves nothing and spends nothing. Ruling:\n`docs/01_decisions/rulings/2026-10-06-the-claude-seat-is-for-intake-\nfixes-and-the-board.md`.","operationId":"record_seat_call_api_rooster_seat_calls_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatCallRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Record Seat Call Api Rooster Seat Calls Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/build-unreachable":{"post":{"tags":["rooster"],"summary":"Record Issue Build Unreachable","description":"`tools/rooster_worker/drain.py` calls this instead of\n`/build-harness-failed` when the build seat itself could not be\nreached AT ALL for this attempt -- not \"ran and could not fix it\", not\n\"our own machinery broke mid-run\", but \"nobody answered\". Frederick,\n2026-08-11: \"the issue just stays filed, and Rooster keeps trying to\nget it filed until the Mac Studio is back online... no different than\nif a developer was sick with the flu for a week.\"\n\nSee `issues_service.record_build_unreachable` for the full contract:\nthis does NOT spend a build attempt and has NO remediation ceiling --\nunlike a harness failure, unreachability is never evidence anything is\nbroken, so it is never a countdown to a self-filed defect, a card, or\na DM. It only ever changes `build_unreachable_since`/`_detail`, which\n`GET` reads of the issue already carry (see `_issue_to_dict`) so a\nperson can see \"queued, seat offline since ...\" on the row itself.","operationId":"record_issue_build_unreachable_api_rooster_issues__issue_id__build_unreachable_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildUnreachableRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Issue Build Unreachable Api Rooster Issues  Issue Id  Build Unreachable Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/issues/{issue_id}/staged":{"post":{"tags":["rooster"],"summary":"Record Staged Fix","description":"Rooster reports that he has fixed it and the fix is on staging.\n\nThis is as far as he goes. Production is Frederick's, and reaching\nfix_staged is what DMs the reporter to go and check the work, because\nnothing else can move until they have.","operationId":"record_staged_fix_api_rooster_issues__issue_id__staged_post","parameters":[{"name":"issue_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Issue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StagedFixRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Staged Fix Api Rooster Issues  Issue Id  Staged Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/pipeline-health":{"get":{"tags":["rooster"],"summary":"Get Pipeline Health","description":"One glanceable read -- see app.modules.audit.health_surface's own\nmodule docstring for why this is DEMOTED: read-only, nothing here ever\nfixes anything, and it exists for Frederick to glance at when curious,\nnever as the mechanism that notices or repairs a stalled pipeline (that\nis `issues_service.release_stalled_builds`, run independently on both\nthe drain loop's own schedule and the audit cron's, plus the synthetic\ncanary -- see tools/audit/run_once.py).","operationId":"get_pipeline_health_api_rooster_pipeline_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Pipeline Health Api Rooster Pipeline Health Get"}}}}}}},"/api/rooster/issues/intake/{conversation_id}/file":{"post":{"tags":["rooster"],"summary":"File Issue From Intake","description":"Rooster files the issue he just wrote.\n\ndesign/Issues.dc.html has no File button in the chat: the reporter\ndescribes the problem and Rooster's turn comes back with a \"Filed\nISS-341\" chip. The draft is still re-validated server side here, so a\nhalf-formed one is refused no matter what the agent claimed.\n\nA REJECTED FILING RECORDS ITSELF, rather than waiting on the agent to\nnotice the 422 and separately call `record_intake_attempt_failed`\nthrough its own tool. That second call was always a matter of the\nagent's own judgment, never enforced -- and Cindy Palma's conversation\n(measured 2026-08-14) shows exactly what happens when it does not\nhappen: her FIRST failure was self-reported and recorded correctly\n(`rooster_intake_attempts.last_failure_reason` set at 19:05:21), but\nnothing recorded any of the retries after it, and the conversation sat\nunfiled for hours with a complete draft nobody was retrying. The\n`pending_for_rooster` fix (see that function's own comment) stops a\nstale record from silently expiring; this stops a failed filing from\never going unrecorded in the first place, which is the narrower and\nmore direct fix -- a write that fails HERE, server-side, needs no agent\ncooperation to be told.","operationId":"file_issue_from_intake_api_rooster_issues_intake__conversation_id__file_post","parameters":[{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Conversation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response File Issue From Intake Api Rooster Issues Intake  Conversation Id  File Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/desks/{label}/session":{"get":{"tags":["rooster"],"summary":"Get Desk Session","description":"What `department_host.py` should `--resume`, if anything -- `None`\nfields mean this desk has never booted (a fresh volume, or a genuine\nfirst run), which is the honest answer, not an error (see\n`desk_state.get_desk_session`'s own docstring).","operationId":"get_desk_session_api_rooster_desks__label__session_get","parameters":[{"name":"label","in":"path","required":true,"schema":{"type":"string","title":"Label"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Desk Session Api Rooster Desks  Label  Session Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["rooster"],"summary":"Put Desk Session","description":"Record what this desk's session actually is, right after it boots or\nresumes -- see `desk_state.upsert_desk_session` for the resume-counter\nsemantics.","operationId":"put_desk_session_api_rooster_desks__label__session_put","parameters":[{"name":"label","in":"path","required":true,"schema":{"type":"string","title":"Label"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeskSessionUpsert"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Desk Session Api Rooster Desks  Label  Session Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/desks/{label}/usage":{"post":{"tags":["rooster"],"summary":"Post Desk Usage","description":"The visibility number, not a cap -- see `desk_state.record_desk_usage`.","operationId":"post_desk_usage_api_rooster_desks__label__usage_post","parameters":[{"name":"label","in":"path","required":true,"schema":{"type":"string","title":"Label"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeskUsageReport"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Desk Usage Api Rooster Desks  Label  Usage Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/desks/{label}/heartbeat":{"post":{"tags":["rooster"],"summary":"Post Desk Heartbeat","description":"Liveness only -- see `desk_state.record_desk_heartbeat`.","operationId":"post_desk_heartbeat_api_rooster_desks__label__heartbeat_post","parameters":[{"name":"label","in":"path","required":true,"schema":{"type":"string","title":"Label"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Post Desk Heartbeat Api Rooster Desks  Label  Heartbeat Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/refine/{skill_key}/evidence":{"get":{"tags":["rooster"],"summary":"Refine Evidence","description":"What the learn pass reads: how this skill is doing, and the failures\nit has not yet learned from.\n\n`enough_evidence` is the server's call, not the worker's, so the decision\n\"is it even worth asking a frontier model about this\" cannot drift\nbetween the two.","operationId":"refine_evidence_api_rooster_refine__skill_key__evidence_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Refine Evidence Api Rooster Refine  Skill Key  Evidence Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/refine/{skill_key}/backlog":{"get":{"tags":["rooster"],"summary":"Refine Edit Backlog","description":"The batch learn pass's read: every edit diff nobody has learned from,\noldest first, keyset-paginated on (created_at, id).\n\nExists beside `/refine/{skill_key}/evidence` rather than replacing it.\nThe evidence endpoint is the 15-minute heartbeat's 50-row steady-state\nread; this one exists to drain a 3,113-diff BACKLOG (the corrections\naccumulated over months while nothing read them -- verified in\nproduction 2026-09-03), which needs pagination and edits-only rows the\nheartbeat read was never shaped for.\n\n`after_created_at`/`after_id` travel together: pass BOTH values from the\nlast row of the previous page, or neither.","operationId":"refine_edit_backlog_api_rooster_refine__skill_key__backlog_get","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}},{"name":"after_created_at","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"After Created At"}},{"name":"after_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"After Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":500,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Refine Edit Backlog Api Rooster Refine  Skill Key  Backlog Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/refine/{skill_key}":{"post":{"tags":["rooster"],"summary":"Propose Refinement","description":"Rooster PROPOSES a change to himself. Nothing is applied here.\n\nFrederick, 2026-08-13: \"He can propose a change and an improvement. And\nthen after I get comfortable with his improvement suggestions... it could\nbe autonomous.\"\n\nThis endpoint used to apply the change directly. It proposes instead,\nbecause a proposal satisfies \"no change that's unprevertable should be\ndone\" outright -- nothing to revert if nothing was changed -- and because\nit is what earns the evidence that decides when the gate can come off.\n\nThe before-rate is measured HERE rather than accepted from the caller: a\nworker reporting its own baseline could report a flattering one, and the\nledger is worthless if the comparison cannot be trusted. When the\nproposal names a vertical, the baseline is that vertical's own rate --\nthe judge later measures the after-rate over the same slice, so the\ncomparison stays like-with-like in both dimensions.","operationId":"propose_refinement_api_rooster_refine__skill_key__post","parameters":[{"name":"skill_key","in":"path","required":true,"schema":{"type":"string","title":"Skill Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordRefinement"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Propose Refinement Api Rooster Refine  Skill Key  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/refine/pending-judgement":{"get":{"tags":["rooster"],"summary":"Refinements Awaiting Judgement","description":"The watch pass's worklist: applied, not yet judged.","operationId":"refinements_awaiting_judgement_api_rooster_refine_pending_judgement_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Refinements Awaiting Judgement Api Rooster Refine Pending Judgement Get"}}}}}}},"/api/rooster/refine/judge/{refinement_id}":{"post":{"tags":["rooster"],"summary":"Judge Refinement","description":"Settle one refinement: kept, rolled back, or not yet.\n\nTAKES NO NUMBERS FROM THE CALLER. The after-rate is measured here, over\nexactly the window that starts when the refinement was applied, and the\nverdict is reached by `refinement.judge`. A worker that reported its own\nbefore/after could report flattering ones, and the entire value of this\nledger is that the comparison can be trusted -- so the only thing the\nwatch pass gets to do is ask.\n\n\"Not yet\" comes back as a normal body with `settled: false`. A young\nrefinement having too little fresh evidence is the expected case, not a\nfault.","operationId":"judge_refinement_api_rooster_refine_judge__refinement_id__post","parameters":[{"name":"refinement_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Refinement Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Judge Refinement Api Rooster Refine Judge  Refinement Id  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/refine/proposals":{"get":{"tags":["rooster"],"summary":"Refinement Proposals","description":"What Rooster is waiting to hear back on, plus how good his suggestions\nhave been so far -- the track record is returned alongside because it is\nthe thing that decides when this gate can come off.\n\n`lessons` (0330) is the per-lesson scoreboard: every approved standing\ninstruction, scored over exactly the drafts that carried it via the\nprovenance stamp. This is the Improvements surface's read of \"which of\nhis changes are actually helping\", and the batch learn pass reads the\nitem count as its open-proposals throttle.","operationId":"refinement_proposals_api_rooster_refine_proposals_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Refinement Proposals Api Rooster Refine Proposals Get"}}}}}}},"/api/rooster/refine/proposals/decided":{"get":{"tags":["rooster"],"summary":"Decided Refinement Proposals","description":"Company OS 2026-09-30 handoff item 6 -- Rooster's \"Improvements\" tab,\nApproved/Declined (`refinement.decided_proposals`'s own docstring has\nthe full contract). Same read gate as `/refine/proposals` above.","operationId":"decided_refinement_proposals_api_rooster_refine_proposals_decided_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Decided Refinement Proposals Api Rooster Refine Proposals Decided Get"}}}}}}},"/api/rooster/refine/proposals/{proposal_id}/decide":{"post":{"tags":["rooster"],"summary":"Decide Refinement Proposal","description":"Approve, edit, or reject a proposed change to Rooster's instructions.\n\nGated on a HUMAN grant, not `rooster:refine` -- the whole point is that he\ncannot answer his own proposal. Approving moves it to `applied`, which is\nwhat the next pass picks up to actually make the edit.","operationId":"decide_refinement_proposal_api_rooster_refine_proposals__proposal_id__decide_post","parameters":[{"name":"proposal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Proposal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideProposal"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decide Refinement Proposal Api Rooster Refine Proposals  Proposal Id  Decide Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/instructions":{"post":{"tags":["rooster-instructions"],"summary":"Create Instruction","description":"Files a directive from Damien, carrying the chairman's own authority\n(migration 0572's own header). Refused for anything other than an api\nkey holding the `instruct` scope -- an ordinary human session reaches\n`current_principal` fine (it is not a read-only key, so the central\nmethod wall never engages) but has no `api_key_scopes` at all, so the\nexplicit check below still refuses it. This is Damien's channel, not a\nsecond way for a human to do the same thing a human's own tools\nalready do.\n\n⛔ENQUEUES ITS OWN PLANNING WORK, IF SOMEBODY IS CONFIGURED TO RUN IT.\nUnset `EXECUTIVE_MANAGER_PRINCIPAL_ID` never blocks filing the\ndirective -- Damien's instruction is real and recorded either way --\nit just has nothing planning it yet, and the response says so plainly\nrather than silently dropping the obligation.","operationId":"create_instruction_api_rooster_instructions_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInstructionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Instruction Api Rooster Instructions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/instructions/{directive_id}":{"get":{"tags":["rooster-instructions"],"summary":"Get Instruction","operationId":"get_instruction_api_rooster_instructions__directive_id__get","parameters":[{"name":"directive_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Directive Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Instruction Api Rooster Instructions  Directive Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["rooster-instructions"],"summary":"Advance Instruction","description":"Moves a directive's status forward -- `instructions_service.advance`\ndoes the legality check (received can never jump straight to executing;\nraised_to_chairman requires a reason); this route is a thin HTTP-status\ntranslation of it, the same shape `require_function` itself is over\n`has_grant`.","operationId":"advance_instruction_api_rooster_instructions__directive_id__patch","parameters":[{"name":"directive_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Directive Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvanceInstructionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Advance Instruction Api Rooster Instructions  Directive Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cases":{"get":{"tags":["rooster-cases"],"summary":"List Cases","description":"Every case not yet closed -- the board's own list, same shape as the\nIssues board it rides alongside (design doc §5).","operationId":"list_cases_api_rooster_cases_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Cases Api Rooster Cases Get"}}}}}}},"/api/rooster/cases/lessons":{"get":{"tags":["rooster-cases"],"summary":"List Lessons","description":"The standing memory `tools/rooster_worker/case_direct.py`'s own\nsystem prompt reads (design doc §3.6 / unit 6) -- registered BEFORE\n`/{case_id}` below so \"lessons\" is never matched as a case id (Starlette\nmatches routes in registration order, not by specificity).","operationId":"list_lessons_api_rooster_cases_lessons_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Lessons Api Rooster Cases Lessons Get"}}}}}}},"/api/rooster/cases/handed":{"get":{"tags":["rooster-cases"],"summary":"List Handed Cases","description":"Company OS 2026-09-30 handoff item 6, Rooster -> Activity tab's\n\"Research cases handed to a person\" -- `service.\nlist_cases_handed_to_a_person`'s own docstring has the full contract.\nRegistered BEFORE `/{case_id}` for the same reason `/lessons` is.","operationId":"list_handed_cases_api_rooster_cases_handed_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Handed Cases Api Rooster Cases Handed Get"}}}}}}},"/api/rooster/cases/{case_id}":{"get":{"tags":["rooster-cases"],"summary":"Get Case","operationId":"get_case_api_rooster_cases__case_id__get","parameters":[{"name":"case_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Case Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Case Api Rooster Cases  Case Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cases/{case_id}/reopen":{"post":{"tags":["rooster-cases"],"summary":"Reopen Case","description":"Design doc unit 3 addendum, 2026-09-05: a person or the coordinator\nreruns a case after fixing a bug in the research loop, without losing\nthe evidence a buggy pass already gathered -- `service.\nreopen_case_for_research`'s own docstring has the full contract.","operationId":"reopen_case_api_rooster_cases__case_id__reopen_post","parameters":[{"name":"case_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Case Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reopen Case Api Rooster Cases  Case Id  Reopen Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/cases/{case_id}/outcome":{"post":{"tags":["rooster-cases"],"summary":"Record Case Outcome","description":"A person disposes of a case handed to them -- `Rooster.dc.html`'s\n\"Close it\" card, \"Mark resolved\". Gated the same as `/reopen` above\n(`rooster:cases:write`, never a per-role business grant): closing a\ncase Rooster asked about is the same \"a person or the coordinator acts\non the case board\" shape, not a new privilege.\n\n`resolved_by_principal_id` is always the calling principal -- a human\nclicked this button, which is exactly what `service.record_outcome`'s\nown docstring says `decision_id`/`resolved_by_principal_id` exist to\ntell apart from Rooster closing a case unattended (`act_switchboard`'s\n`resolved_by_principal_id=None`).","operationId":"record_case_outcome_api_rooster_cases__case_id__outcome_post","parameters":[{"name":"case_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Case Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordOutcomeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Case Outcome Api Rooster Cases  Case Id  Outcome Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies":{"get":{"tags":["growth-strategies"],"summary":"List Strategies","description":"Every strategy, side by side -- optionally narrowed to one goal.","operationId":"list_strategies_api_growth_strategies_get","parameters":[{"name":"goal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Goal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Strategies Api Growth Strategies Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}":{"get":{"tags":["growth-strategies"],"summary":"Get Strategy Route","operationId":"get_strategy_route_api_growth_strategies__strategy_id__get","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Strategy Route Api Growth Strategies  Strategy Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/results":{"post":{"tags":["growth-strategies"],"summary":"Record Result","description":"Log one result by hand -- the bridge until U6 wires automatic\nattribution (see `growth_strategy_events`'s own table comment,\nmigration 0661). Frederick's 2026-09-25 rule stands here too: this\nroute RECORDS a result that already happened; it never sends anything.","operationId":"record_result_api_growth_strategies__strategy_id__results_post","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Result Api Growth Strategies  Strategy Id  Results Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/messaging":{"get":{"tags":["growth-strategies"],"summary":"List Messaging Versions","description":"Ruling gap #2338 Q4: every messaging version this strategy has ever\nproposed, each carrying its OWN approval state -- never the strategy's\nown `decision_id`.","operationId":"list_messaging_versions_api_growth_strategies__strategy_id__messaging_get","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Messaging Versions Api Growth Strategies  Strategy Id  Messaging Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["growth-strategies"],"summary":"Propose Messaging Version Route","description":"Ruling gap #2338 Q4, verbatim: \"Plan approval does NOT approve the\nmessaging; each messaging version gets its own approval.\" Creates the\nversion AND files its own decision in one call -- a version sitting\nwith no decision at all is not a real proposal, it is a draft nobody\nasked to see.","operationId":"propose_messaging_version_route_api_growth_strategies__strategy_id__messaging_post","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Propose Messaging Version Route Api Growth Strategies  Strategy Id  Messaging Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/messaging/{version_id}":{"get":{"tags":["growth-strategies"],"summary":"Get Messaging Version Route","description":"One version plus the REAL, measured tally behind its\n`winning_variant` (ruling gap #2338 Q5) -- so a person reading the\ncall can see the counts a recommendation rests on, not just the\nlabel.","operationId":"get_messaging_version_route_api_growth_strategies__strategy_id__messaging__version_id__get","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Version Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Messaging Version Route Api Growth Strategies  Strategy Id  Messaging  Version Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/thread":{"get":{"tags":["growth-strategies"],"summary":"Get Thread","description":"The strategy's own standing conversation -- U3's \"Plan panel with\nthe thread\" on the goal card. Empty (never 404) for a strategy with no\nconversation yet: nobody has talked about it, which is a real state,\nnot an error.","operationId":"get_thread_api_growth_strategies__strategy_id__thread_get","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Thread Api Growth Strategies  Strategy Id  Thread Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/messaging/{version_id}/variant-events":{"post":{"tags":["growth-strategies"],"summary":"Record Variant Event Route","description":"Ruling gap #2338 Q5: a measured event (sent/replied/meeting_booked)\nfor one wording variant, inside an already-APPROVED messaging version\n-- never on one still awaiting its own click. Recomputes the winner\n(if there is now enough signal) on every call, so a person reading\n`winning_variant` never has to run a separate step.","operationId":"record_variant_event_route_api_growth_strategies__strategy_id__messaging__version_id__variant_events_post","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Version Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Variant Event Route Api Growth Strategies  Strategy Id  Messaging  Version Id  Variant Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/budget-lines":{"get":{"tags":["growth-strategies"],"summary":"List Budget Lines","operationId":"list_budget_lines_api_growth_strategies__strategy_id__budget_lines_get","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Budget Lines Api Growth Strategies  Strategy Id  Budget Lines Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["growth-strategies"],"summary":"Create Budget Line Route","description":"Ruling gap #2338 Q13, verbatim: \"A strategy can say 'ACA booth, $X,\nbecause N target agencies attend'; he approves the line with the\nplan.\" No decision filed here on purpose -- `status` starts\n'proposed' and moves with the strategy's own decision\n(`growth.messaging.advance_budget_lines_with_strategy`, run every\n`strategy_tick_worker` pass), never a second click.","operationId":"create_budget_line_route_api_growth_strategies__strategy_id__budget_lines_post","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Budget Line Route Api Growth Strategies  Strategy Id  Budget Lines Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/growth/strategies/{strategy_id}/talk":{"post":{"tags":["growth-strategies"],"summary":"Talk","description":"U3 -- talk this strategy over with Rooster. The Roost surface's own\ndoor (the web) into `strategy_talk.post_and_answer`; U5's Slack DM\nreaches the SAME function directly (`supervision_routes.py`), never\nthis HTTP route -- see that module's own comment for why.","operationId":"talk_api_growth_strategies__strategy_id__talk_post","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strategy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Talk Api Growth Strategies  Strategy Id  Talk Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/qa/ask":{"post":{"tags":["rooster-qa"],"summary":"Ask","operationId":"ask_api_rooster_qa_ask_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/reflex/webhook/episode-processed":{"post":{"tags":["rooster-reflex"],"summary":"Episode Processed Webhook","description":"Fast-path re-scan of the ONE graph named in the payload. 404s when\n`ROOSTER_REFLEX_WEBHOOK_ENABLED` is unset -- indistinguishable from \"this\nroute does not exist\" to an unauthenticated caller, the safest default\nfor a webhook this codebase cannot yet verify the signature of.","operationId":"episode_processed_webhook_api_rooster_reflex_webhook_episode_processed_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Episode Processed Webhook Api Rooster Reflex Webhook Episode Processed Post"}}}}}}},"/api/rooster/mcp":{"post":{"tags":["rooster-mcp"],"summary":"Mcp Endpoint","description":"The whole protocol, on one URL.\n\nMCP's streamable-HTTP transport posts either a single JSON-RPC message or\na batch of them here. A request earns a response; a notification earns\n`202 Accepted` with no body, which is how the transport says \"received,\nnothing to say back\".","operationId":"mcp_endpoint_api_rooster_mcp_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/rooster/supervision/report":{"post":{"tags":["rooster-supervision"],"summary":"Report Finding","description":"Rooster reports a finding, a fix, or an escalation -- see\nsupervision.py's module docstring for the three shapes and Frederick's\nown words behind each one. `urgent=True` sends immediately; otherwise\nthe item waits for the next digest sweep (`SupervisionDigestWorker`).","operationId":"report_finding_api_rooster_supervision_report_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportFindingRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Report Finding Api Rooster Supervision Report Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/supervision/resolve":{"post":{"tags":["rooster-supervision"],"summary":"Report Resolution","description":"A condition previously reported under `body.reference` is now\nobserved healthy -- closes the loop on whatever open item matches (see\nsupervision.record_resolution's own docstring for how it ties the\nclearance to the specific open item and refuses to fire twice). `POST\n/report` with `urgent=True` is what RAISES a defect/escalation;\n`apps/rooster/entrypoint.sh:probe_brain` calls this one only from the\nbranch where its own probe actually answered \"SEAT OK\" -- never merely\nbecause nothing failed this boot.\n\n`resolved: false` (with `item: null`) is the ordinary, expected answer\non most calls: nothing was open under this reference, so nothing was\nposted -- silence, not an error.","operationId":"report_resolution_api_rooster_supervision_resolve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResolutionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Report Resolution Api Rooster Supervision Resolve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/supervision/inbox":{"get":{"tags":["rooster-supervision"],"summary":"Get Inbox","description":"Items carrying at least one reply Rooster has not yet acted on --\nwhat a future recipe/worker reads to know what changed (see\ndocs/ROOSTER_SUPERVISION_CHANNEL.md, \"what this deliberately does\nnot do\" -- wiring an actual consumer of this read is the next step,\nnot this build).","operationId":"get_inbox_api_rooster_supervision_inbox_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Inbox Api Rooster Supervision Inbox Get"}}}}}}},"/api/rooster/supervision/items/{item_id}/acknowledge":{"post":{"tags":["rooster-supervision"],"summary":"Acknowledge Item Replies","description":"Rooster has acted on every unconsumed reply for this item.","operationId":"acknowledge_item_replies_api_rooster_supervision_items__item_id__acknowledge_post","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Acknowledge Item Replies Api Rooster Supervision Items  Item Id  Acknowledge Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/supervision/items":{"get":{"tags":["rooster-supervision"],"summary":"List Items","operationId":"list_items_api_rooster_supervision_items_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Items Api Rooster Supervision Items Get"}}}}}}},"/api/rooster/supervision/items/{item_id}":{"get":{"tags":["rooster-supervision"],"summary":"Get Item","operationId":"get_item_api_rooster_supervision_items__item_id__get","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Item Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Item Api Rooster Supervision Items  Item Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/supervision/slack/interactivity":{"post":{"tags":["rooster-supervision"],"summary":"Slack Interactivity","description":"Slack's Interactivity target: a button tap on Rooster's DM asking\nwhether to send a floor person home early (#3571, Frederick 2026-10-03;\n`floor_blocks.send_home`). The ONE Slack interactivity surface in Roost\n-- a Slack app has exactly one Interactivity Request URL, so every\nbutton family dispatches from this loop by `action_id`: send-home,\nbank-rule batches and expense flags (#3669), salary-payment\nconfirmations (#3711), and, since 2026-10-04\n(FLOOR-GPS-SCHEDULER-001 S5b, #2981), the postage ask's \"Load $X\" /\n\"Wait\" buttons (`rooster.postage_taps.handle_tap`; this docstring used\nto say they were \"still a typed yes\" -- CORRECTED 2026-10-04, left\nstanding here as the record of what was believed).\n\nAuthenticated exactly like `/slack/events`: the HMAC signature over the\nraw body is the whole boundary. Slack posts `application/x-www-form-\nurlencoded` with the JSON in a `payload` field. A tap counts only from\nthe configured recipient's own Slack user (the same check a typed reply\ngets); anyone else's tap is ignored. The answer itself is claim-once\n(`send_home.resolve`), so a double tap, a tap after a typed reply and the\ntimeout racing a tap are one answer.\n\n⛔NEEDS A SLACK-CONSOLE STEP nobody can do from code: the Rooster app's\nInteractivity Request URL must point here\n(`docs/runbooks/FLOOR_SEND_HOME_APPROVAL.md`). Until then a typed reply\nin the DM thread, and the timeout (which keeps the person working),\ncarry the decision.","operationId":"slack_interactivity_api_rooster_supervision_slack_interactivity_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Slack Interactivity Api Rooster Supervision Slack Interactivity Post"}}}}}}},"/api/rooster/supervision/slack/events":{"post":{"tags":["rooster-supervision"],"summary":"Slack Events","description":"Slack's Events API target. No `Depends` grant -- see this module's\nown docstring for why the signature check below is the entire\nauthentication boundary.\n\nReads the raw body BEFORE any parsing, same reason\napps/chatwoot/src/server.ts's own webhook does: the signature is over\nthe exact bytes Slack sent, and re-serialising parsed JSON produces a\ndifferent string that never verifies.","operationId":"slack_events_api_rooster_supervision_slack_events_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Slack Events Api Rooster Supervision Slack Events Post"}}}}}}},"/api/rooster/team_chat/principals/{principal_id}/record":{"get":{"tags":["rooster-team-chat"],"summary":"Get Durable Record","description":"What a bounded run would load right now for this principal -- the\nsame assembly `service.handle_inbound_message` calls, exposed for\ninspection (and for Rooster's own tools, per this router's own\ndocstring on why nothing here needs a web caller).","operationId":"get_durable_record_api_rooster_team_chat_principals__principal_id__record_get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Durable Record Api Rooster Team Chat Principals  Principal Id  Record Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/principals/{principal_id}/messages":{"get":{"tags":["rooster-team-chat"],"summary":"Get Recent Messages","operationId":"get_recent_messages_api_rooster_team_chat_principals__principal_id__messages_get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Recent Messages Api Rooster Team Chat Principals  Principal Id  Messages Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/discussions/{discussion_id}/thread":{"get":{"tags":["rooster-team-chat"],"summary":"Get Discussion Thread","operationId":"get_discussion_thread_api_rooster_team_chat_discussions__discussion_id__thread_get","parameters":[{"name":"discussion_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Discussion Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Discussion Thread Api Rooster Team Chat Discussions  Discussion Id  Thread Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/principals/{principal_id}/brief":{"put":{"tags":["rooster-team-chat"],"summary":"Put Brief","operationId":"put_brief_api_rooster_team_chat_principals__principal_id__brief_put","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetBriefRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Brief Api Rooster Team Chat Principals  Principal Id  Brief Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/questions/pending":{"get":{"tags":["rooster-team-chat"],"summary":"Pending Questions","description":"What the `rooster` worker's `fetch_pending` drains -- the same\n`{\"conversations\": [{\"id\": ...}]}` shape every other pending surface\nuses, so no new client code. Derived, not claimed: `claim` below is\nthe claim.","operationId":"pending_questions_api_rooster_team_chat_questions_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Questions Api Rooster Team Chat Questions Pending Get"}}}}}}},"/api/rooster/team_chat/questions/{question_id}/claim":{"post":{"tags":["rooster-team-chat"],"summary":"Claim Question","description":"Take the question and get everything the seat needs for one turn:\nthe system prompt, the user payload, the asker. 409 when another\nworker already holds it -- the conditional UPDATE in `questions.claim`\nis the referee.","operationId":"claim_question_api_rooster_team_chat_questions__question_id__claim_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Question Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimQuestionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Claim Question Api Rooster Team Chat Questions  Question Id  Claim Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/questions/{question_id}/answer":{"post":{"tags":["rooster-team-chat"],"summary":"Answer Question","description":"The seat's answer: posted to Slack where the question was asked,\nrecorded on the row and in `team_chat_messages`, the seat's tokens\nfiled in `model_calls`. 409 when the row was already answered (the\nfallback got there first, or a duplicate post).","operationId":"answer_question_api_rooster_team_chat_questions__question_id__answer_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Question Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerQuestionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Answer Question Api Rooster Team Chat Questions  Question Id  Answer Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/questions/{question_id}/seat-failed":{"post":{"tags":["rooster-team-chat"],"summary":"Seat Failed","description":"The seat could not answer: paused on its allowance (this row backs\noff and is retried), or failed outright (the fallback takes it).","operationId":"seat_failed_api_rooster_team_chat_questions__question_id__seat_failed_post","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Question Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeatFailedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Seat Failed Api Rooster Team Chat Questions  Question Id  Seat Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/questions/{question_id}":{"get":{"tags":["rooster-team-chat"],"summary":"Get Question","description":"The audit row: what was asked, what Rooster looked at, what he\nanswered, who answered, what it cost.","operationId":"get_question_api_rooster_team_chat_questions__question_id__get","parameters":[{"name":"question_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Question Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Question Api Rooster Team Chat Questions  Question Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/team_chat/measure":{"get":{"tags":["rooster-team-chat"],"summary":"Measure Answering","description":"The standing measurement: questions answered, time to first seen,\ntime to answer, who answered, refusals, escalations, thumbs.","operationId":"measure_answering_api_rooster_team_chat_measure_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":168,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Measure Answering Api Rooster Team Chat Measure Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/floor/turns/pending":{"get":{"tags":["rooster-floor-turns"],"summary":"Pending Turn","description":"Claim the oldest pending turn and return it with its packet, in the\n`{\"conversations\": [{\"id\": ...}]}` shape every pending surface uses (the worker's\n`fetch_pending` reads that key). An empty list means nothing is waiting. A turn\nclaimed here and never answered goes back to `pending` after\n`floor_turn_queue.CLAIM_LEASE`.","operationId":"pending_turn_api_rooster_floor_turns_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Turn Api Rooster Floor Turns Pending Get"}}}}}}},"/api/rooster/floor/turns/{turn_id}":{"get":{"tags":["rooster-floor-turns"],"summary":"Get Turn","description":"What the runner needs for one claimed turn: both prompts, the Qwen endpoint and\ndefault model (`engine`, from the same `team_chat_seat_engine` setting the Slack seat\nreads: one engine setting for every Qwen harness lane) and `lane_engine`, which\nADAPTER runs it (`rooster_lane_engine`; no row = `claude_cli` on that same Qwen\nengine). 404 when the turn is not claimed (done, failed, or its claim lapsed).","operationId":"get_turn_api_rooster_floor_turns__turn_id__get","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Turn Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Turn Api Rooster Floor Turns  Turn Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/floor/turns/{turn_id}/decision":{"post":{"tags":["rooster-floor-turns"],"summary":"Record Turn Decision","description":"Validate the lane's answer, file its cost, record the decision and end the turn.\n\nOrder matters. (1) The run's cost is filed in `model_calls` FIRST, success or not,\nbecause the tokens were spent either way (a late answer included). (2) The turn must\nstill be `claimed`: a late answer from a worker whose claim lapsed is 409, never a\nsecond decision (its spend is filed as a failed call).\n(3) A malformed answer fails the turn with the reason and answers 422. (4) A valid\none goes through `floor_turns.record_decision` against the packet the claim\nstored, with the `model_calls` id; the row read back is the receipt.","operationId":"record_turn_decision_api_rooster_floor_turns__turn_id__decision_post","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Turn Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Turn Decision Api Rooster Floor Turns  Turn Id  Decision Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/floor/turns/{turn_id}/failed":{"post":{"tags":["rooster-floor-turns"],"summary":"Turn Failed","description":"The lane could not run this turn (no seat, a timeout, a nonzero exit). The turn\nends `failed` with `reason`, with `retry` goes back to `pending` while attempts\nremain, and with `paused` goes back without spending an attempt. When a model ran\n(`model` set) its spend is filed first as a failed `model_calls` row, before the\n409 check, so a turn that is no longer ours still has its tokens counted. 409 when\nthe turn is not claimed (already done or reclaimed).","operationId":"turn_failed_api_rooster_floor_turns__turn_id__failed_post","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Turn Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Turn Failed Api Rooster Floor Turns  Turn Id  Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/executive/turns/pending":{"get":{"tags":["rooster-executive-turns"],"summary":"Pending Turn","description":"Claim the oldest pending turn, in the `{\"conversations\": [...]}` shape every pending\nsurface uses. Empty means nothing waits. An unanswered claim lapses after `CLAIM_LEASE`.","operationId":"pending_turn_api_rooster_executive_turns_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Pending Turn Api Rooster Executive Turns Pending Get"}}}}}}},"/api/rooster/executive/turns/{turn_id}/result":{"post":{"tags":["rooster-executive-turns"],"summary":"Record Turn Result","description":"File the run's cost, store the answer and end the turn `done`. A late answer (not\nclaimed UNDER THE DIGEST ECHOED) is 409 and files nothing: the cost row and the ending\nshare ONE transaction. An empty answer fails the turn, files a failed call, and is 422.","operationId":"record_turn_result_api_rooster_executive_turns__turn_id__result_post","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Turn Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecutiveTurnResultRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Turn Result Api Rooster Executive Turns  Turn Id  Result Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/rooster/executive/turns/{turn_id}/failed":{"post":{"tags":["rooster-executive-turns"],"summary":"Turn Failed","description":"The seat could not run this turn. It ends `failed` with `reason`, or with `retry`\ngoes back to `pending` while attempts remain. 409 when not claimed under the digest.","operationId":"turn_failed_api_rooster_executive_turns__turn_id__failed_post","parameters":[{"name":"turn_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Turn Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecutiveTurnFailedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Turn Failed Api Rooster Executive Turns  Turn Id  Failed Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/roles":{"get":{"tags":["workforce"],"summary":"Scheduling Switches","description":"Which roles Rooster rosters, and which it does not.\n\nReadable by anyone signed in: whether your own job is scheduled is not a\nsecret from you.","operationId":"scheduling_switches_api_workforce_roles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Scheduling Switches Api Workforce Roles Get"}}}}}}},"/api/workforce/roles/{role_name}/scheduling":{"post":{"tags":["workforce"],"summary":"Set Scheduling","description":"The switch. Frederick: \"I turn it on [per role] ... it's by the roles.\"\n\nGated on people:write rather than a scheduling-specific grant: deciding that\na job is rostered by machine is an employment decision, and it belongs with\nwhoever already administers people.","operationId":"set_scheduling_api_workforce_roles__role_name__scheduling_post","parameters":[{"name":"role_name","in":"path","required":true,"schema":{"type":"string","title":"Role Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Set Scheduling Api Workforce Roles  Role Name  Scheduling Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/roster/{role_name}/{week_start}/publish":{"post":{"tags":["workforce"],"summary":"Publish Roster Week","description":"Tell the team what they are working.\n\n⛔THE STEP THAT DID NOT EXIST. `roster_weeks.published_at` has been in the\nschema since migration 0276 and nothing ever set it, while `my_week` reads\nonly published weeks -- so every planned roster was invisible to the people\nit was planned for. `plan_week.py` wrote proposals into a room with no door.\n\nGated on people:write, the same grant that decides a role is rostered by\nmachine at all: telling somebody the hours they are working is an\nemployment act, and it belongs with whoever administers people.","operationId":"publish_roster_week_api_workforce_roster__role_name___week_start__publish_post","parameters":[{"name":"role_name","in":"path","required":true,"schema":{"type":"string","title":"Role Name"}},{"name":"week_start","in":"path","required":true,"schema":{"type":"string","format":"date","title":"Week Start"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Publish Roster Week Api Workforce Roster  Role Name   Week Start  Publish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/time-off":{"get":{"tags":["workforce"],"summary":"My Time Off","description":"Your own asks and what came back. Your own only -- there is no\nprincipal_id parameter here for the same reason `my-week` has none.","operationId":"my_time_off_api_workforce_time_off_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Time Off Api Workforce Time Off Get"}}}}}},"post":{"tags":["workforce"],"summary":"Ask For Time Off","description":"Ask for days off, and get the answer in the same breath.\n\n⛔UNGATED ON PURPOSE, FOR YOURSELF ONLY. Asking for a day off is not a\nprivilege somebody grants: every person on the roster can ask, and the\nprincipal is taken from the session rather than the body so nobody can\nask on somebody else's behalf by editing a field.\n\nThe answer is Rooster's and it is immediate -- the rule is the roster's\nown coverage rule, so waiting on a human to apply it would be ceremony.\nFrederick, 2026-09-04: \"he should always try to optimize an employee's\nrequest if possible\", which is why a no comes back with the nearest days\nthat would be a yes.","operationId":"ask_for_time_off_api_workforce_time_off_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeOffRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ask For Time Off Api Workforce Time Off Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/days-off":{"get":{"tags":["workforce"],"summary":"My Days Off","description":"The \"Days I can't work\" card: the rule, when the schedule posts, the\nfirst day that can still be blocked, and every upcoming blocked day.\n\nFrederick, 2026-09-23: one place on Today (and the same inside the /floor\nAvailability tab) for a contractor to block off vacation and personal\ndays. Your own only -- the principal is the session, never a parameter.\n`applies` is False for an employee, whose Time off tab is unchanged.","operationId":"my_days_off_api_workforce_days_off_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Days Off Api Workforce Days Off Get"}}}}}},"post":{"tags":["workforce"],"summary":"Block Days Off","description":"Block off a range. Before the schedule posts it is accepted at once --\nno approval, no refusal, never counted against you (`days_off.block`).\nAfter it posts, 409 with what to do instead: swap, or call out.","operationId":"block_days_off_api_workforce_days_off_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DaysOffBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Block Days Off Api Workforce Days Off Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/days-off/day/{day}":{"delete":{"tags":["workforce"],"summary":"Unblock One Day","description":"Take back one blocked day -- the /floor Availability grid's tap.","operationId":"unblock_one_day_api_workforce_days_off_day__day__delete","parameters":[{"name":"day","in":"path","required":true,"schema":{"type":"string","format":"date","title":"Day"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unblock One Day Api Workforce Days Off Day  Day  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/days-off/{block_id}":{"delete":{"tags":["workforce"],"summary":"Unblock Days Off","description":"The card's Remove: takes back every day of a block that is still\nahead and not yet posted. A posted day stays -- it was planned around.","operationId":"unblock_days_off_api_workforce_days_off__block_id__delete","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unblock Days Off Api Workforce Days Off  Block Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/call-out":{"post":{"tags":["workforce"],"summary":"Call Out","description":"Call out sick for a day, from the app. Your own only.\n\nFrederick, 2026-09-23: \"calling out in the app is never a no-show. 12+\nhours' notice is always free. 2 FREE sick call-outs per 90 days (within\n12h); after that, a within-12h call-out is a strike. No doctor's note.\"\n\nThe day's shifts are released and cover is asked for\n(`cover.report_out`, the same path Rooster's own `report_out_sick`\ntakes); for a contractor each released shift is priced by the rules and\nthe answer comes back here: what it cost and how many free sick call-outs\nare left. The asks are sent by the executive engine's own tick\n(`cover.resume_unattempted_asks`), as every recorded ask is.","operationId":"call_out_api_workforce_call_out_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallOutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Call Out Api Workforce Call Out Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/time-off/waiting":{"get":{"tags":["workforce"],"summary":"Time Off Waiting On A Human","description":"What a supervisor still owes an answer on.","operationId":"time_off_waiting_on_a_human_api_workforce_time_off_waiting_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Time Off Waiting On A Human Api Workforce Time Off Waiting Get"}}}}}}},"/api/workforce/time-off/{request_id}/decide":{"post":{"tags":["workforce"],"summary":"Decide Time Off","description":"A supervisor's answer, over Rooster's.\n\nRooster decides by the rule; a supervisor decides by everything the rule\ncannot see -- somebody covering for a friend, a funeral, a visa\nappointment that cannot move. Approving here releases the shifts exactly\nas an approval by rule does, so the roster never disagrees with the\nanswer somebody was given.","operationId":"decide_time_off_api_workforce_time_off__request_id__decide_post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Request Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeOffDecision"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decide Time Off Api Workforce Time Off  Request Id  Decide Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/my-week":{"get":{"tags":["workforce"],"summary":"My Week","description":"A person's own rostered hours.\n\n⛔THE PERK, IN FREDERICK'S FRAMING (2026-08-26): \"rooster can show\nemployees that they worked this many hours ... that they're not working as\nmany hours because it's AI assisted.\" The break time this used to report\nbeside the hours is retired (Frederick, 2026-10-04: \"We don't do schedule\nbreaks anymore. They're contractors. They can take their own breaks when\nthey want to take their own breaks.\"): the hours are the rostered hours,\nwith nothing taken off for a break.\n\nTheir own row only. No principal_id parameter exists here on purpose --\nreading somebody else's hours goes through the manager view below, which is\ngated.","operationId":"my_week_api_workforce_my_week_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Week Api Workforce My Week Get"}}}}}}},"/api/workforce/my-shifts":{"get":{"tags":["workforce"],"summary":"My Shifts","description":"Your own next shifts, with the ids a swap needs.\n\n⛔`my-week` COULD NOT ANSWER THIS. It returns hours -- what a person is\nowed -- and never the shifts themselves, so there was no\nway to point at one and say \"this is the day I need covered\".\n\nPublished and unreleased only, and only ahead: a shift already worked is\nnot something anybody can take off your hands.","operationId":"my_shifts_api_workforce_my_shifts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Shifts Api Workforce My Shifts Get"}}}}}}},"/api/workforce/swaps":{"get":{"tags":["workforce"],"summary":"Shifts On Offer","description":"What a colleague could pick up. Your own offers are excluded -- they\nare yours to withdraw, not to take.","operationId":"shifts_on_offer_api_workforce_swaps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Shifts On Offer Api Workforce Swaps Get"}}}}}},"post":{"tags":["workforce"],"summary":"Offer A Shift","description":"Put one of your own shifts up for a colleague to take.\n\n⛔NOT AN APPROVAL QUEUE. Frederick, 2026-09-04: \"rooster allows them to\nswap shifts around amongst themselves as long as they show up to work.\"\nYour own shift only, and the session says whose it is.\n\n⛔AND IT IS NO LONGER ONLY A BOARD. Putting a shift up is an EMPLOYEE\nREQUEST (Track E12), so it gets an owner in the same breath: a row in\n`employee_requests` whose `owner_work_id` is a `swap_shift` piece of\nexecutive work. Rooster then finds an eligible replacement, asks them,\nplaces the winner through `swaps.take` and tells both sides -- instead of\nthe offer sitting on a board until a colleague happens to look.\n\n⛔THE KEY IS THE OFFER'S OWN ID, WHICH MAKES THIS IDEMPOTENT FOR FREE.\n`swaps.offer` upserts on `one live offer per shift`, so posting twice\nreturns the same offer and therefore the same request and the same work\n-- never a second round of messages to real people.","operationId":"offer_a_shift_api_workforce_swaps_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapOffer"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Offer A Shift Api Workforce Swaps Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/swaps/{offer_id}/take":{"post":{"tags":["workforce"],"summary":"Take A Shift","description":"Take an offered shift. The shift moves to you, or nothing moves.\n\nRefused when you are already working those hours, or when the desk would\nbe left under two people or all in one country -- the same rule the week\nwas built on, not a second idea of what coverage means.","operationId":"take_a_shift_api_workforce_swaps__offer_id__take_post","parameters":[{"name":"offer_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Offer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Take A Shift Api Workforce Swaps  Offer Id  Take Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/swaps/{offer_id}/withdraw":{"post":{"tags":["workforce"],"summary":"Withdraw A Shift Offer","description":"Take your own offer back off the board.","operationId":"withdraw_a_shift_offer_api_workforce_swaps__offer_id__withdraw_post","parameters":[{"name":"offer_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Offer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Withdraw A Shift Offer Api Workforce Swaps  Offer Id  Withdraw Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/utilisation":{"get":{"tags":["workforce"],"summary":"Team Utilisation","description":"Hours promised against hours worked, per person.\n\n⛔THE 90-DAY REVIEW ASKS THIS AND NOTHING COULD ANSWER IT. The task\nRooster raises on 8 October (migration 0341) says what to bring: hours\nscheduled against hours actually worked, time off asked for and taken,\nand who was reachable when their shift said they would be. Each of those\nfacts lived in a different table and none had ever been read together.\n\nFrederick, 2026-09-04, on why it is counted at all: \"allocate those\nresources just how we allocate technology resources, tokens for\ninferences, and server costs and everything else.\"\n\nGated on people:write like the team view above -- these are the numbers\na person's job is judged on and they are nobody else's business.","operationId":"team_utilisation_api_workforce_utilisation_get","parameters":[{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":90,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Team Utilisation Api Workforce Utilisation Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/backlog":{"get":{"tags":["workforce"],"summary":"Read Backlog","description":"The open pile declarations, and what Roost can see for itself.","operationId":"read_backlog_api_workforce_backlog_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Read Backlog Api Workforce Backlog Get"}}}}}},"post":{"tags":["workforce"],"summary":"Declare Backlog","description":"Tell Roost about a pile it cannot see.\n\n⛔THIS IS THE ONLY WAY THE REAL BACKLOG REACHES THE ROSTER, and it stays\nthat way until Centric is ours. Frederick, 2026-08-25: \"You have not taken\nover Centric, so there's no way for you to see what's upstream. You don't\nsee a parcel until it's scanned to be put in a box for the first time.\"\n\nSo the number a person can see with their eyes -- \"about five thousand\nparcels, we don't have ink, ink comes tomorrow\" -- is typed in, and the\nroster does the rest. No grant beyond being signed in: anyone standing in\nthe room can count it, and a wrong count is corrected by the next one\nrather than prevented by a permission.","operationId":"declare_backlog_api_workforce_backlog_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Declare Backlog Api Workforce Backlog Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/backlog/{declaration_id}/cleared":{"post":{"tags":["workforce"],"summary":"Clear Backlog","description":"The pile is gone. Closing it out stops it staffing another day.","operationId":"clear_backlog_api_workforce_backlog__declaration_id__cleared_post","parameters":[{"name":"declaration_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Declaration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Clear Backlog Api Workforce Backlog  Declaration Id  Cleared Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/on-the-clock":{"get":{"tags":["workforce"],"summary":"Who Is On The Clock","description":"Everybody on the clock at `at` (default now), everybody scheduled then\nand not in, and for the customer-service desk what they did and what sat.\n\n⛔READ-ONLY, AND \"UNKNOWN\" IS A THIRD ANSWER. When the clock cannot be\nread, `clock_available` is false and `on_the_clock` is null -- never an\nempty list, which would read as \"nobody came\".","operationId":"who_is_on_the_clock_api_workforce_on_the_clock_get","parameters":[{"name":"at","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"At"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Who Is On The Clock Api Workforce On The Clock Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/desk-ratings":{"get":{"tags":["workforce"],"summary":"Desk Ratings","description":"The customer-service desk's score: the rolling window that decides the\ntier, its trend week by week, the recent shifts, and why.\n\nA manager (or anybody holding the People read) sees everyone they\nmanage; everybody else sees exactly their own row, and a caller who is\nnot on the desk sees an empty list.\n\n⛔B4a (#3504), Frederick, 2026-10-03: measured in production this route\ncost 5,058ms/162 queries for Frederick, 2,901ms/132 for a CS rep, and\n2,654ms/128 for a floor contractor who gets an empty list -- every one\nof those computed the IDENTICAL whole-desk reading (`desk_activity.\nreport`'s `only` narrows what is returned, never what is read -- see its\ndocstring). Two fixes, in order:\n\n1. A caller whose visible rows (`_desk_rows_visible_to`) hold nobody\n   who is actually on the desk today (`desk_activity.desk_members_among`\n   -- one cheap existence check) gets the empty body immediately,\n   without ever reading a shift, a completion or waiting work for\n   anyone.\n2. Everybody else reads through `desk_reading_cache.cached_report`: the\n   whole desk computed once and shared by every caller for up to 120\n   seconds (see that module's docstring for the staleness reasoning),\n   filtered down to exactly the rows this caller may see\n   (`desk_reading_cache.filter_rows`) -- byte-identical to what calling\n   `report(..., only=visible)` would have returned on its own.","operationId":"desk_ratings_api_workforce_desk_ratings_get","parameters":[{"name":"weeks","in":"query","required":false,"schema":{"type":"integer","default":6,"title":"Weeks"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Desk Ratings Api Workforce Desk Ratings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/standing-shift":{"get":{"tags":["workforce"],"summary":"My Standing Shift","description":"Your standing shift this period and, once announced, next period's, in\nyour own time -- plus whether you asked to keep it. Frederick: the next\nquarter's shift is announced \"2 weeks before the quarter starts (Today\nplus Slack), so nobody is surprised\". `current` is None for anybody with\nno standing shift (the floor, an employee). Your own only.","operationId":"my_standing_shift_api_workforce_standing_shift_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Standing Shift Api Workforce Standing Shift Get"}}}}}}},"/api/workforce/standing-shift/keep":{"put":{"tags":["workforce"],"summary":"Keep My Shift","description":"\"I'd like to keep my shift\" (Frederick, 2026-09-23, rule 5 of\n`standing_schedule`). Rooster honours it at the next rotation when\nfairness allows, and logs when it can't.","operationId":"keep_my_shift_api_workforce_standing_shift_keep_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeepMyShiftBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Keep My Shift Api Workforce Standing Shift Keep Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/supervisor-training":{"post":{"tags":["workforce"],"summary":"Set Supervisor Training","description":"A dated window in which two desk supervisors may share a shift.\n\nFrederick, 2026-09-24: \"They always have to work opposite shifts\" -- and,\nthe same day, \"Next week I will have Gagandeep work the same shift as\nMark for training.\" So the exception is a WINDOW with a start, an end\nand a reason, set by a manager; the rule applies again by itself the day\nafter it ends. Gated on people:write, the same grant that publishes a\nroster: it changes what a published schedule is allowed to hold.","operationId":"set_supervisor_training_api_workforce_supervisor_training_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrainingWindowBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Set Supervisor Training Api Workforce Supervisor Training Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/me/cover-ask-link/{token}":{"get":{"tags":["workforce"],"summary":"Open Cover Ask Link","description":"Where a cover ask's one-tap link lands. READ ONLY, the same rule\n`floor_blocks.routes.open_offer_link` follows: a link preview or a chat\nunfurler fetches a URL without anybody tapping it, so nothing here may\naccept -- `POST .../accept` below is the only thing that writes an\nanswer.\n\n⛔AND ONLY TO THE PERSON IT WAS SENT TO. The link names an ask; the\nsession names who is looking. Somebody else's link opens nothing.","operationId":"open_cover_ask_link_api_workforce_me_cover_ask_link__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Cover Ask Link Api Workforce Me Cover Ask Link  Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/me/cover-asks/{ask_id}/accept":{"post":{"tags":["workforce"],"summary":"Accept Cover Ask Link","description":"One tap accepts a cover ask -- from the link's page.\n\n⛔`outcome.reason` IS THE REAL ANSWER, NOT `outcome.handled`.\n`employee_reply.accept_cover_ask` always reports `handled=True` for a\ncover ask -- `record_answer`'s own rule, \"the arrival is recorded\nwhatever the verdict\" -- so a late or already-answered tap still comes\nback `handled=True` with `reason` naming why it did not apply. The\n`state != 'open'` check above catches the common case; this catches the\nrace where it changed between that read and this write.","operationId":"accept_cover_ask_link_api_workforce_me_cover_asks__ask_id__accept_post","parameters":[{"name":"ask_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ask Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Accept Cover Ask Link Api Workforce Me Cover Asks  Ask Id  Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/me/swap-ask-link/{token}":{"get":{"tags":["workforce"],"summary":"Open Swap Ask Link","description":"Where a swap ask's one-tap link lands. See `open_cover_ask_link`'s\nown docstring; the two are identical except for the table.","operationId":"open_swap_ask_link_api_workforce_me_swap_ask_link__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Swap Ask Link Api Workforce Me Swap Ask Link  Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/me/swap-asks/{ask_id}/accept":{"post":{"tags":["workforce"],"summary":"Accept Swap Ask Link","description":"One tap accepts a swap ask -- from the link's page. Unlike cover,\n`swaps.record_answer` reports `recorded=False` outright for a late or\nalready-answered ask, so `outcome.handled` alone is the real answer\nhere.","operationId":"accept_swap_ask_link_api_workforce_me_swap_asks__ask_id__accept_post","parameters":[{"name":"ask_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Ask Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Accept Swap Ask Link Api Workforce Me Swap Asks  Ask Id  Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/training":{"get":{"tags":["workforce"],"summary":"List Training","description":"Every training in progress or upcoming for one team -- `Floor\nSchedule.dc.html`'s Training section, CHANGES.md §2. Gated on the same\n`hr:schedule:read` grant `/api/floor/hr/schedule` already reads under.","operationId":"list_training_api_workforce_training_get","parameters":[{"name":"team","in":"query","required":true,"schema":{"enum":["fulfillment","customer_service"],"type":"string","title":"Team"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Training Api Workforce Training Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["workforce"],"summary":"Create Training","description":"Put any number of trainees on a trainer's blocks for a dated window,\nnew hire or retraining -- CHANGES.md §2. Gated on `people:write`, the\nsame grant `/supervisor-training` above already uses: both change what\na published schedule is allowed to hold.\n\n⛔RETRAINING DOES NOT PULL THE TRAINEE OFF THEIR OWN BLOCKS YET.\nCHANGES.md §2, verbatim: \"Retraining does not yet pull the person off\ntheir usual blocks on the grid. Frederick to decide; build as the\ndesign shows.\" This route only ever writes `workforce_trainings`/\n`workforce_training_trainees` (see `training.py`'s own header) -- it\nnever touches `floor_blocks`/`roster_shifts`, so nobody is removed from\na shift they already hold by calling it.","operationId":"create_training_api_workforce_training_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrainingCreateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Training Api Workforce Training Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/training/roster":{"get":{"tags":["workforce"],"summary":"Training Roster","description":"The trainee search list and trainer options for one team's Training\nform. Not itself part of CHANGES.md §2's named API shape (that names\nonly the write) -- the form's picker cannot populate without a read, so\nthis is the minimal one it needs.","operationId":"training_roster_api_workforce_training_roster_get","parameters":[{"name":"team","in":"query","required":true,"schema":{"enum":["fulfillment","customer_service"],"type":"string","title":"Team"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Training Roster Api Workforce Training Roster Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workforce/training/{training_id}":{"delete":{"tags":["workforce"],"summary":"Delete Training","description":"Remove a training row (the design's text-link \"Remove\"). Soft\ndelete (`removed_at`), same discipline as `contractor_certifications.\nrevoked_at`: the row's history stays, only its effect on the grid\nstops.","operationId":"delete_training_api_workforce_training__training_id__delete","parameters":[{"name":"training_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Training Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Training Api Workforce Training  Training Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors":{"get":{"tags":["contractors"],"summary":"List Contractors","operationId":"list_contractors_api_contractors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Contractors Api Contractors Get"}}}}}}},"/api/contractors/people":{"get":{"tags":["contractors"],"summary":"People Tab","description":"The People tab -- one record for everybody, with their lifecycle.\n\n⛔GATED BY `hr:contractors:read`, WHICH IS THE WHOLE CROSS-PERSON RULE.\nREADME §11: *\"Let a contractor see another person's numbers\"* is on the\ndo-not list, and the guarantee that enforces it already exists -- see\n`floor_blocks/routes.py::get_contractor_numbers`, which refuses any\ncontractor asking for an id that is not their own. This route carries\nevery person's standing, progress and open management work in one body,\nso it is HR-only outright: there is no \"your own row\" arm to fall back\nto, because a contractor has no business reading this list at all.\n\n⛔AND IT IS DECLARED BEFORE `/{contractor_id}`. FastAPI matches in\ndeclaration order, so a literal path registered after the parameterised\none is swallowed by it and answers 404 for a route that exists -- which\nis why `/documents` and `/performance` sit here too.","operationId":"people_tab_api_contractors_people_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response People Tab Api Contractors People Get"}}}}}}},"/api/contractors/documents":{"get":{"tags":["contractors"],"summary":"Documents Tab","operationId":"documents_tab_api_contractors_documents_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Documents Tab Api Contractors Documents Get"}}}}}}},"/api/contractors/performance":{"get":{"tags":["contractors"],"summary":"Performance Tab","description":"The Performance tab's fleet table -- everyone side by side, William's\nview only (`hr:contractors:read`; the contractor's own equivalent is\n`GET /api/floor/me/numbers`, one row, gated `floor:contractor`).\n\n⛔DEPARTMENT-SCOPED SINCE 2026-10-02 (S1, HR People team switch):\n`?department=customer_service` reads `performance.cs_fleet_view`\n(first-reply vs best) instead of the floor's own `fleet_view` (pace vs\nbest) -- two genuinely different metrics over two different\npopulations, never one query branching mid-body. An unrecognised value\nrefuses rather than silently falling back to the floor's own rows.","operationId":"performance_tab_api_contractors_performance_get","parameters":[{"name":"department","in":"query","required":false,"schema":{"type":"string","default":"fulfillment","title":"Department"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Performance Tab Api Contractors Performance Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/offer":{"get":{"tags":["contractors"],"summary":"Cs Offer","description":"The ONE shareable offer link -- Frederick, 2026-09-23: \"I'll just\nsend the link out myself to everyone.\" No token: whoever is signed in\nsees their OWN offer, or \"this offer isn't for you\" if the outsourced\nCS desk role isn't theirs.\n\n⛔DECLARED BEFORE `/{contractor_id}`, same reason `/people`/`/documents`/\n`/performance` are -- see `people_tab`'s own docstring.","operationId":"cs_offer_api_contractors_offer_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Cs Offer Api Contractors Offer Get"}}}}}}},"/api/contractors/offer/sign":{"post":{"tags":["contractors"],"summary":"Sign Cs Offer","operationId":"sign_cs_offer_api_contractors_offer_sign_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignCSOfferBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Sign Cs Offer Api Contractors Offer Sign Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/desk-signing":{"get":{"tags":["contractors"],"summary":"Desk Signing","description":"The HR contractors surface's own progress list for the offer link --\nFrederick, 2026-09-23: \"show the link, and who has and hasn't signed\n... so Frederick can copy it and see progress.\" `hr:contractors:read`,\nsame as every other HR read in this router; there is no self-service\narm because this names every desk person at once.","operationId":"desk_signing_api_contractors_desk_signing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Desk Signing Api Contractors Desk Signing Get"}}}}}}},"/api/contractors/rules":{"get":{"tags":["contractors"],"summary":"Contractor Rules","description":"The /rules page: the rules in Frederick's words, and -- for the person\nasking -- their own score, strikes and free sick call-outs.\n\n⛔ONE SOURCE FOR EVERY NUMBER: the questions and the strike table come\nfrom `contractors.rules_text`, built from the same constants the\nenforcement reads (and that the signed agreement states). ⛔ANYBODY\nSIGNED IN MAY READ THE RULES; `me` is always the caller's own row, never\nanybody else's.\n\n⛔DEPARTMENT-SCOPED SINCE 2026-10-01: `rules_text.faq_for_department`/\n`strike_table_for_department` replace `faq()`/`strike_table()` here --\nsee those functions' own docstrings and `docs/01_decisions/rulings/\n2026-10-01-one-contractor-agreement-per-department.md`. `faq()`/\n`strike_table()` themselves are UNCHANGED (still read by the onboarding\nphone line and email composer, out of scope for this change).","operationId":"contractor_rules_api_contractors_rules_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Contractor Rules Api Contractors Rules Get"}}}}}}},"/api/contractors/{contractor_id}":{"get":{"tags":["contractors"],"summary":"Get Contractor","operationId":"get_contractor_api_contractors__contractor_id__get","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Contractor Api Contractors  Contractor Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/certifications/{step}/history":{"get":{"tags":["contractors"],"summary":"Certification History","operationId":"certification_history_api_contractors__contractor_id__certifications__step__history_get","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}},{"name":"step","in":"path","required":true,"schema":{"type":"string","title":"Step"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Certification History Api Contractors  Contractor Id  Certifications  Step  History Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/hire":{"post":{"tags":["contractors"],"summary":"Hire","operationId":"hire_api_contractors_hire_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HireBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Hire Api Contractors Hire Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/certify":{"post":{"tags":["contractors"],"summary":"Certify","operationId":"certify_api_contractors__contractor_id__certify_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertifyBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Certify Api Contractors  Contractor Id  Certify Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/background-check":{"post":{"tags":["contractors"],"summary":"Record Background Check Result","description":"Record a background check result by hand -- Frederick's 2026-10-01\nruling on a per-stage requirement named this as the still-missing\nmanual door (`service.record_background_check_result`'s own\ndocstring); no vendor adapter calls this route today, a person does.\nSame `hr:contractors:write` grant every other contractor-record write\non this router already needs.","operationId":"record_background_check_result_api_contractors__contractor_id__background_check_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordBackgroundCheckResultBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Record Background Check Result Api Contractors  Contractor Id  Background Check Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/promote-onboarded":{"post":{"tags":["contractors"],"summary":"Promote Onboarded","description":"Promote a `training` contractor to `assignable` on Frederick's\n2026-09-25 ruling (master-plan unit 2, \"A block reaches a contractor\"):\n\"Everyone fully onboarded\" -- any active contractor who finished\nonboarding and the ID check. See `service.\npromote_when_onboarded_and_id_verified`'s own docstring for how this\nreconciles with `certify`'s 2026-09-19 \"non-negotiable\" attestation\nrule. Same `contractors:write` grant `certify` needs.\n\n⛔SUPERSEDED IN PART, 2026-09-28 (issue #2335): \"a person clicks this,\nper contractor; nothing calls it on a timer\" was true when this route\nwas built and is no longer the ordinary path -- `service.\ntry_auto_promote_when_ready` now calls this same underlying function\nautomatically, from the onboarding-completion and ID-check-verified\nevents. This route is the manual door: the Contractors-page \"Promote\"\nbutton (`apps/web/src/pages/hr/contractors/PeopleTab.tsx`) calls it for\nthe case the automatic hook cannot cover -- a contractor who was already\n`training`, onboarded and verified before the hook existed.","operationId":"promote_onboarded_api_contractors__contractor_id__promote_onboarded_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Promote Onboarded Api Contractors  Contractor Id  Promote Onboarded Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/revoke":{"post":{"tags":["contractors"],"summary":"Revoke","operationId":"revoke_api_contractors__contractor_id__revoke_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revoke Api Contractors  Contractor Id  Revoke Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/safety-stop":{"post":{"tags":["contractors"],"summary":"Safety Stop","description":"A safety, conduct or equipment-damage problem: the contractor is\nstopped now and sent home, and the review goes to William (floor), Mark\n(desk) or Frederick (Frederick's contractor rules, 2026-09-23). See\n`corrective_action.stop_for_safety`.\n\n⛔GATED `hr:contractors:write`: stopping somebody is a manager's act, and\nthe person who reports it is recorded as the one who took it.\n\nSUPERSEDED: `POST /{contractor_id}/clear_pause` lifted a strike pause.\nThere are no pauses any more; a stop is undone by `/{id}/stop/reverse`.","operationId":"safety_stop_api_contractors__contractor_id__safety_stop_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SafetyStopBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Safety Stop Api Contractors  Contractor Id  Safety Stop Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/stop/reverse":{"post":{"tags":["contractors"],"summary":"Reverse Stop","description":"A person overrides the stop standing on this contractor: their shifts\ncome back, the record stays. \"Unless a human overrides it\" -- the conduct\narm's posture, kept by Frederick's 2026-09-23 rules for every stop\n(`corrective_action.reverse`).","operationId":"reverse_stop_api_contractors__contractor_id__stop_reverse_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseStopBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reverse Stop Api Contractors  Contractor Id  Stop Reverse Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/end-engagement":{"post":{"tags":["contractors"],"summary":"End Engagement","description":"A manager ends an active contractor's engagement directly -- no\nstrike, no performance freeze, no safety report behind it. Build task,\n2026-10-01, Frederick: \"there's no way for me to remove anyone.\" See\n`service.end_engagement`'s own docstring: this calls the exact same\n`end_shifts` every ladder stop calls, so the floor give-back-with-surge\nand the desk cover-ask run identically.\n\n⛔GATED `hr:contractors:write`, THE SAME GRANT `hire` NEEDS. Build task:\n\"Same permission as hiring.\" Ending somebody's engagement is a manager's\nact of exactly the same weight as starting one.","operationId":"end_engagement_api_contractors__contractor_id__end_engagement_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EndEngagementBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response End Engagement Api Contractors  Contractor Id  End Engagement Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/withdraw-invite":{"post":{"tags":["contractors"],"summary":"Withdraw Invite","description":"Withdraw an invite somebody never finished onboarding on. Build task,\n2026-10-01, same ruling as `end_engagement` above. Expires their unused\nonboarding links at once, marks unsigned documents withdrawn, switches\noff their login, and takes them off the active roster and People list --\na soft removal, kept for history. Refused (409) if they already signed\nthe IC agreement and worked a shift: `end_engagement` is the right\naction for them instead.","operationId":"withdraw_invite_api_contractors__contractor_id__withdraw_invite_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawInviteBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Withdraw Invite Api Contractors  Contractor Id  Withdraw Invite Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/resend-onboarding-link":{"post":{"tags":["contractors"],"summary":"Resend Onboarding Link","description":"Plain \"Resend link\" for someone still mid-onboarding. Build task,\n2026-10-01, item 2. A fresh token and a real send through the same\ncomposer `hire` uses -- the old link is left exactly as it was.","operationId":"resend_onboarding_link_api_contractors__contractor_id__resend_onboarding_link_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resend Onboarding Link Api Contractors  Contractor Id  Resend Onboarding Link Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/strikes/{strike_id}/dispute":{"post":{"tags":["contractors"],"summary":"Dispute Strike","description":"The contractor says they could not clock in, and why.\n\nFrederick, 2026-09-17: a way for the contractor to report \"couldn't clock\nin\" and for the department manager to clear the strike. This is their half;\n`waive_strike` above is the manager's.\n\n⛔GATED `floor:contractor` AND SCOPED TO THEMSELVES. The contractor id\ncomes from the signed-in principal, never from the body -- a route that\ntook it from the request would let anybody dispute anybody's strike, and\nthe service checks the strike belongs to them as well.\n\n⛔AND IT RAISES A CARD RATHER THAN WAIVING. A contractor saying a strike\nwas not their fault is a claim, not a finding; if reporting cleared it,\nthe three-strikes rule would be unenforceable by anyone who objects.","operationId":"dispute_strike_api_contractors_strikes__strike_id__dispute_post","parameters":[{"name":"strike_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strike Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Dispute Strike Api Contractors Strikes  Strike Id  Dispute Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/strikes/{strike_id}/waive":{"post":{"tags":["contractors"],"summary":"Waive Strike","description":"Clear a strike the contractor could not have avoided.\n\nFrederick, 2026-09-17: if somebody cannot clock in because of OUR\nequipment or systems -- a device not registered, a network not configured,\nintake refused for a missing tub count -- no strike stands.\n\n⛔THE ROW IS KEPT, NEVER DELETED. It is the only evidence the failure was\nours rather than theirs, and the party it protects is the contractor. See\n`service.waive_strike` and migration 0498.\n\nThe reason is required and is refused blank by the service AND by the\ndatabase's own CHECK -- a waiver with no stated cause is a strike that\nvanished, which is the thing keeping the row was meant to prevent.","operationId":"waive_strike_api_contractors_strikes__strike_id__waive_post","parameters":[{"name":"strike_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Strike Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Waive Strike Api Contractors Strikes  Strike Id  Waive Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/contractors/{contractor_id}/countersign":{"post":{"tags":["contractors"],"summary":"Countersign","description":"The manager signs what the contractor signed, and both parties get the\nexecuted copy.\n\n⛔THE SIGNER IS WHOEVER IS SIGNED IN, never a name in the body. A\ncountersignature that could name somebody else would be worth nothing as\nevidence, and this route is behind `hr:contractors:write` for the same\nreason the rest of this module's writes are.\n\nThe ip and user agent are taken from the request itself, exactly as the\ncontractor's own signature records them.","operationId":"countersign_api_contractors__contractor_id__countersign_post","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Countersign Api Contractors  Contractor Id  Countersign Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/onboard/{token}":{"get":{"tags":["contractor-onboarding-public"],"summary":"Get Onboarding","description":"⛔BUILD TASK, 2026-10-01, ITEM 3: \"Clicking an expired link shows 'A\nnew link has been sent to <contact>', never a dead page.\" Calls\n`get_onboarding_or_reissue`, never the plain `get_onboarding` a 410\nwould come from -- an expired (but once-real) token mints and sends a\nfresh one on the spot; only a token that never existed still 404s.","operationId":"get_onboarding_api_onboard__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Onboarding Api Onboard  Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/onboard/{token}/payout-requirements":{"get":{"tags":["contractor-onboarding-public"],"summary":"Payout Requirements","description":"What Wise needs to pay somebody in `country`, asked of Wise right now.\n\n⛔THE FORM IS DRAWN FROM THIS, NOT FROM ANYTHING WE HOLD. Frederick,\n2026-09-15: \"the system should be designed to ask for information based\noff the country in real time. When they're filling out the form, versus\ntrying to think through all the different countries at once.\"\n\nPublic and token-scoped like the rest of onboarding: the contractor has\nno login, and the link IS the credential. The Wise token never leaves the\nserver -- this is why the browser cannot simply call Wise itself.\n\nAnswers `payable: false` with a sentence rather than an error when Wise\nwill not pay into that country: a contractor reading \"500\" learns nothing,\nand it is a real state, not a fault.\n\n⛔ADDED 2026-10-01 (fix/the-bank-form-is-never-rate-limited-mid-entry):\n`details` is a JSON object of this country's fields the contractor has\nalready answered (`{\"legalType\": \"BUSINESS\"}`), sent by the page ONLY\nfor a field Wise's own response marked `refreshRequirementsOnChange`\n(`wise.RequiredField.refresh_requirements_on_change`) -- choosing\n\"Business\" over \"Person\" changes what Wise asks for next, and this is\nhow the page re-asks for the UPDATED list without ever touching the\nsubmit route. A malformed or empty `details` degrades to the plain\ncountry-only ask, never a 400 -- a refresh is advisory, not a\nsubmission, and nothing about it should be able to fail the form.","operationId":"payout_requirements_api_onboard__token__payout_requirements_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"country","in":"query","required":true,"schema":{"type":"string","title":"Country"}},{"name":"details","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Details"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Payout Requirements Api Onboard  Token  Payout Requirements Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/onboard/{token}/id-check/start":{"post":{"tags":["contractor-onboarding-public"],"summary":"Start Onboarding Id Check","description":"Open a Vouched-HOSTED ID check for the person holding this onboarding\nlink: stores what they typed, has Vouched create the verification with\nthe secret key, and returns Vouched's own link and QR code. Frederick,\n2026-09-24: \"We need to verify ID\" and \"We don't use a public key.\" Three\npath segments, so it can never be mistaken for `/{token}/{kind}` below.","operationId":"start_onboarding_id_check_api_onboard__token__id_check_start_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartIdCheckBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Start Onboarding Id Check Api Onboard  Token  Id Check Start Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/onboard/{token}/id-check/{reference}/done":{"post":{"tags":["contractor-onboarding-public"],"summary":"Finish Onboarding Id Check","description":"The page's \"I've finished\" for the onboarding link. The body is\nempty: Roost follows the attempt's own invite to its job and reads it\nfrom Vouched with the secret key, and decides from THAT (receipt\ndiscipline). Returns the whole onboarding view, so the packet unlocks in\none round trip once the ID is verified.","operationId":"finish_onboarding_id_check_api_onboard__token__id_check__reference__done_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"reference","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Reference"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Finish Onboarding Id Check Api Onboard  Token  Id Check  Reference  Done Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/onboard/{token}/{kind}":{"post":{"tags":["contractor-onboarding-public"],"summary":"Submit Onboarding Item","operationId":"submit_onboarding_item_api_onboard__token___kind__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"kind","in":"path","required":true,"schema":{"type":"string","title":"Kind"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Submit Onboarding Item Api Onboard  Token   Kind  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vendor-pay/{token}":{"get":{"tags":["vendor-registration-public"],"summary":"Get Page","description":"Who the page is for. An expired link mints and emails a fresh one to the\ncontact on file and says so (CON-39); with no contact on file it says to ask\nPigeonly, and never claims a send that did not happen.","operationId":"get_page_api_vendor_pay__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Page Api Vendor Pay  Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["vendor-registration-public"],"summary":"Register","description":"File who they are and how to pay them. Good once per link.","operationId":"register_api_vendor_pay__token__post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VendorRegistrationIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Register Api Vendor Pay  Token  Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vendor-pay/{token}/payout-requirements":{"get":{"tags":["vendor-registration-public"],"summary":"Payout Requirements","description":"What Wise needs to pay somebody in `country`, asked right now: the same\nform contractors fill (`wise.requirements_for_form`). The link is proven\nfirst; the Wise credential never leaves the server.","operationId":"payout_requirements_api_vendor_pay__token__payout_requirements_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}},{"name":"country","in":"query","required":true,"schema":{"type":"string","title":"Country"}},{"name":"details","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Details"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Payout Requirements Api Vendor Pay  Token  Payout Requirements Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/webhooks/vouched":{"post":{"tags":["vouched-webhook"],"summary":"Vouched Webhook","description":"Vouched's POST of a finished (or re-reviewed) ID check.\n\n⛔THE SIGNATURE IS THE ONLY THING THAT MAKES THIS TRUSTED. `X-Signature`\nis base64(HMAC-SHA1) of the RAW body under our private key (or the\naccount's signature key) -- Vouched's Webhooks page. It is checked on\n`await request.body()`, the exact bytes received, before the body is\nparsed; a forged or unsigned POST gets 401 and nothing is read or\nwritten (`id_check.record_from_webhook`). `ref` is the attempt's own\nreference, which Roost put in the `callbackURL` when the attempt began.\n\nA genuine webhook Roost has no attempt for is acknowledged (200,\n`recorded: false`) so Vouched does not retry it for ever.","operationId":"vouched_webhook_api_webhooks_vouched_post","parameters":[{"name":"ref","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Vouched Webhook Api Webhooks Vouched Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/hr/policies":{"get":{"tags":["hr-policies"],"summary":"Policies And Authority","operationId":"policies_and_authority_api_hr_policies_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Policies And Authority Api Hr Policies Get"}}}}}}},"/api/floor/blocks/{block_id}/move":{"post":{"tags":["floor-blocks"],"summary":"Move Block","description":"Move the block a person holds to a new day and hours, and tell them.\n\nThe manager action the product lacked until 2026-10-08 (Frederick, relaying\nWilliam: put Elizabeth on the morning block and cancel the late one). The\nwhole rule, and what it deliberately does not do, is in\n`app.modules.floor_blocks.move`; this route only parses the time and maps\nthe typed refusals (404 no such block, 409 the block is not accepted or has\nstarted or the person already holds that time, 422 a time a block cannot\nhave).\n\nGATED `hr:contractors:write`: the grant that already lets a manager hire,\ncertify, pause and end an engagement for a floor contractor, so no grant\nwas invented. A caller without it gets the platform's 403 before anything\nis read.","operationId":"move_block_api_floor_blocks__block_id__move_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveBlockBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Move Block Api Floor Blocks  Block Id  Move Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/blocks/{block_id}/remove":{"post":{"tags":["floor-blocks"],"summary":"Remove Block","description":"Remove a block nobody has accepted (a draft or an offered block), and keep\nthe scheduler from posting it again for that window.\n\nFrederick, 2026-10-09, on the Saturday 4-8 PM block of the week of 2026-10-12:\n\"2 should be enough, 2 sort and scan, one print, 1-2 packing and shipping\" and\n\"for the packaging and shipping we can add blocks if we need to\". The whole\nrule, and what it deliberately does not do, is in\n`app.modules.floor_blocks.removal`; how the engine and the daily pattern read\nthe removal is `app.modules.floor_blocks.manager_holds`. This route maps the\ntyped refusals: 404 no such block, 409 somebody has the block (\"Someone has\nthis block. Move it or talk to them.\") or it is in a state a removal does not\napply to, 422 a reason shorter than `MIN_REMOVAL_REASON_CHARS`.\n\nGATED `hr:contractors:write`, the grant `move_block` and the other manager\nfloor actions use, so no grant was invented. A caller without it gets the\nplatform's 403 before anything is read.","operationId":"remove_block_api_floor_blocks__block_id__remove_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveBlockBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Block Api Floor Blocks  Block Id  Remove Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/shifts":{"get":{"tags":["floor-blocks"],"summary":"Get Contractor Shifts","operationId":"get_contractor_shifts_api_floor_contractor_shifts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Contractor Shifts Api Floor Contractor Shifts Get"}}}}}}},"/api/floor/contractor/picks":{"get":{"tags":["floor-blocks"],"summary":"Get Contractor Picks","description":"The request window: when it closes, the caller's credits, the blocks\nthey may ask for, and -- once it closes -- what they got and why.","operationId":"get_contractor_picks_api_floor_contractor_picks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Contractor Picks Api Floor Contractor Picks Get"}}}}}},"post":{"tags":["floor-blocks"],"summary":"Request Pick","description":"Ask for a window while the request window is open. Asking again sets\nthe rank. When somebody asks never decides who gets it.\n\nFrederick, 2026-10-09: the person picks a time window, the server picks\nthe block, and a request that overlaps one the person already made is\nrefused in plain words with nothing written (409).","operationId":"request_pick_api_floor_contractor_picks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PickBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Request Pick Api Floor Contractor Picks Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/picks/{block_id}":{"delete":{"tags":["floor-blocks"],"summary":"Withdraw Pick","operationId":"withdraw_pick_api_floor_contractor_picks__block_id__delete","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Withdraw Pick Api Floor Contractor Picks  Block Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/availability":{"get":{"tags":["floor-blocks"],"summary":"Get Contractor Availability","operationId":"get_contractor_availability_api_floor_contractor_availability_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Contractor Availability Api Floor Contractor Availability Get"}}}}}}},"/api/floor/me/numbers":{"get":{"tags":["floor-blocks"],"summary":"Get My Numbers","description":"The phone's own \"My numbers\" tab -- ALWAYS the calling principal's\nown `contractors` row. Bundle README §1: \"Scorecard exposure: own\nnumbers only, never another person's\" -- there is no `contractor_id`\nanywhere in this request, on purpose, the same floor `/contractor/*`\nalready holds.","operationId":"get_my_numbers_api_floor_me_numbers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get My Numbers Api Floor Me Numbers Get"}}}}}}},"/api/floor/contractors/{contractor_id}/numbers":{"get":{"tags":["floor-blocks"],"summary":"Get Contractor Numbers","description":"The same numbers, addressable by id -- for William's Contractors page\nto read one row, or a contractor's own phone to read itself by id. Never\nanother contractor's: allowed only with `hr:contractors:read`, or when\nthe caller's OWN `contractors` row is the one asked for. README §6 do-\nnot: \"Let a contractor see another contractor's numbers\" -- proved by\n`test_routes.py`'s 403 on a second contractor's id.","operationId":"get_contractor_numbers_api_floor_contractors__contractor_id__numbers_get","parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Contractor Numbers Api Floor Contractors  Contractor Id  Numbers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/accept":{"post":{"tags":["floor-blocks"],"summary":"Accept Block","operationId":"accept_block_api_floor_contractor_blocks__block_id__accept_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Accept Block Api Floor Contractor Blocks  Block Id  Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/decline":{"post":{"tags":["floor-blocks"],"summary":"Decline Block","operationId":"decline_block_api_floor_contractor_blocks__block_id__decline_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Decline Block Api Floor Contractor Blocks  Block Id  Decline Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/call-out":{"post":{"tags":["floor-blocks"],"summary":"Call Out Block","description":"Give back a block you accepted: sick (`sick=true`) or for any other\nreason. Frederick, 2026-09-23: calling out is never a no-show; twelve\nhours' notice is free; inside twelve hours two sick call-outs per 90 days\nare free, then a strike; giving a block back inside twelve hours for any\nother reason is a strike. `service.call_out_block`.","operationId":"call_out_block_api_floor_contractor_blocks__block_id__call_out_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallOutBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Call Out Block Api Floor Contractor Blocks  Block Id  Call Out Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/sent-home":{"post":{"tags":["floor-blocks"],"summary":"Sent Home Block","description":"\"I was sent home\": the person names the manager who sent them home from\nthis block, and Rooster asks that manager in Slack (`floor_blocks.sent_home`).\n\nFrederick, 2026-10-06, #4038: \"when they are sent home they need a button to\npress that send a slack to william to approve to keep them from getting a\nstrike\" / \"So we don't have ppl gaming the system\". This ASKS and changes\nnothing about any strike: only the named manager's approval clears one. Once\nper block (409 on a second ask); 403 for a block that is not yours; 409 when\nthe block is not about to start or is over; 503 when the manager could not be\nreached on Slack (nothing is recorded that waits on an answer nobody got).","operationId":"sent_home_block_api_floor_contractor_blocks__block_id__sent_home_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SentHomeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sent Home Block Api Floor Contractor Blocks  Block Id  Sent Home Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/swap":{"post":{"tags":["floor-blocks"],"summary":"Swap Block","operationId":"swap_block_api_floor_contractor_blocks__block_id__swap_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Swap Block Api Floor Contractor Blocks  Block Id  Swap Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/swap-takers":{"get":{"tags":["floor-blocks"],"summary":"Swap Takers","operationId":"swap_takers_api_floor_contractor_blocks__block_id__swap_takers_get","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Swap Takers Api Floor Contractor Blocks  Block Id  Swap Takers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/contractor/blocks/{block_id}/swap/accept":{"post":{"tags":["floor-blocks"],"summary":"Accept Swap","operationId":"accept_swap_api_floor_contractor_blocks__block_id__swap_accept_post","parameters":[{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Accept Swap Api Floor Contractor Blocks  Block Id  Swap Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/instruction/link/{token}":{"get":{"tags":["floor-blocks"],"summary":"Open Instruction Link","description":"Where a floor instruction's signed SMS link lands: shows the\ninstruction's own text and acknowledges it `via=\"link\"` -- the ONLY\nthing this route does. The web page that renders this payload is NOT\nbuilt by this slice (see the PR body's open items); this is the API\nhalf only, same split `open_offer_link` already uses for the surge\noffer's own link.\n\n⛔GATED BY SESSION **AND** BY THE SIGNED LINK, same posture\n`open_offer_link` already takes: the token names an instruction; the\nsession must be the principal it was sent to, or this 403s rather than\nshow (or acknowledge) somebody else's instruction.","operationId":"open_instruction_link_api_floor_instruction_link__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Instruction Link Api Floor Instruction Link  Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/me/offer-link/{token}":{"get":{"tags":["floor-blocks"],"summary":"Open Offer Link","description":"Where a surge offer's link lands. READ ONLY, ON PURPOSE: a link\npreview or a chat unfurler fetches a URL without anybody tapping it, so\nnothing here may accept. It shows the offer; `take`/`pass` below act.\n\n⛔AND ONLY TO THE PERSON IT WAS SENT TO. The link names an offer; the\nsession names who is looking. Somebody else's link opens nothing.","operationId":"open_offer_link_api_floor_me_offer_link__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Open Offer Link Api Floor Me Offer Link  Token  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/me/live-offers":{"get":{"tags":["floor-blocks"],"summary":"My Live Offers","description":"The viewer's open time-sensitive offers, for the Today page. Anybody\nsigned in may ask; somebody who is not a contractor simply has none.","operationId":"my_live_offers_api_floor_me_live_offers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response My Live Offers Api Floor Me Live Offers Get"}}}}}}},"/api/floor/me/offers/{offer_id}/take":{"post":{"tags":["floor-blocks"],"summary":"Take Live Offer","description":"One tap takes it -- from the link's page or the Today card.","operationId":"take_live_offer_api_floor_me_offers__offer_id__take_post","parameters":[{"name":"offer_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Offer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Take Live Offer Api Floor Me Offers  Offer Id  Take Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/me/offers/{offer_id}/pass":{"post":{"tags":["floor-blocks"],"summary":"Pass Live Offer","description":"Passing on a surge offer never counts against anybody.","operationId":"pass_live_offer_api_floor_me_offers__offer_id__pass_post","parameters":[{"name":"offer_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Offer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pass Live Offer Api Floor Me Offers  Offer Id  Pass Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/tub-counts":{"get":{"tags":["floor-blocks-tubs"],"summary":"Get Tub Counts","operationId":"get_tub_counts_api_fulfillment_tub_counts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Tub Counts Api Fulfillment Tub Counts Get"}}}}}},"post":{"tags":["floor-blocks-tubs"],"summary":"Post Tub Counts","operationId":"post_tub_counts_api_fulfillment_tub_counts_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TubCountBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Tub Counts Api Fulfillment Tub Counts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fulfillment/sort-counts":{"get":{"tags":["floor-blocks-tubs"],"summary":"Get Sort Counts","operationId":"get_sort_counts_api_fulfillment_sort_counts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Sort Counts Api Fulfillment Sort Counts Get"}}}}}},"post":{"tags":["floor-blocks-tubs"],"summary":"Post Sort Counts","operationId":"post_sort_counts_api_fulfillment_sort_counts_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SortCountBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Sort Counts Api Fulfillment Sort Counts Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/surge/voice-answer/{surge_run_id}/{contractor_id}":{"post":{"tags":["floor-blocks-public"],"summary":"Surge Voice Answer","description":"The call's menu: the offer, then \"press 1 to accept\".","operationId":"surge_voice_answer_api_floor_surge_voice_answer__surge_run_id___contractor_id__post","parameters":[{"name":"surge_run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Surge Run Id"}},{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/surge/voice-answer/{surge_run_id}/{contractor_id}/digits":{"post":{"tags":["floor-blocks-public"],"summary":"Surge Voice Digits","description":"Frederick, 2026-09-23: \"press 1 to accept\". The key goes through\n`service.accept_offer` -- the same lock and the same refusals every other\naccept gets -- and the answer is said back, so a person on the phone\nknows whether the block is theirs before they hang up.","operationId":"surge_voice_digits_api_floor_surge_voice_answer__surge_run_id___contractor_id__digits_post","parameters":[{"name":"surge_run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Surge Run Id"}},{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/instruction/voice-answer/{instruction_id}/{contractor_id}":{"post":{"tags":["floor-blocks-public"],"summary":"Instruction Voice Answer","description":"The instruction call's menu: speak the instruction, then ask whether\nit was heard and whether it can be done.","operationId":"instruction_voice_answer_api_floor_instruction_voice_answer__instruction_id___contractor_id__post","parameters":[{"name":"instruction_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Instruction Id"}},{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/instruction/voice-answer/{instruction_id}/{contractor_id}/digits":{"post":{"tags":["floor-blocks-public"],"summary":"Instruction Voice Digits","description":"Press 1: heard it, have it (`instructions.acknowledge`, `via=\"call\"`).\nPress 2: heard it, cannot do it (`instructions.mark_cannot`, which fires\nthe bench and staff chain at once). Anything else: nothing recorded.","operationId":"instruction_voice_digits_api_floor_instruction_voice_answer__instruction_id___contractor_id__digits_post","parameters":[{"name":"instruction_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Instruction Id"}},{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/clock-in":{"post":{"tags":["floor-clock"],"summary":"Clock In Route","description":"Workstation-only, by construction: the gate below is checked before\nanything else, and no parameter anywhere in this body can skip it. A\nrefusal is recorded (`enforce_device_gate`) before the 403.","operationId":"clock_in_route_api_floor_clock_in_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClockInBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Clock In Route Api Floor Clock In Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/clock-out":{"post":{"tags":["floor-clock"],"summary":"Clock Out Route","description":"No device gate on the way out -- README's rule is about the clock\nSTARTING at a workstation; #1443's own overrun worker already auto-\ncloses a shift nobody ends, and refusing an honest clock-out from\nwherever the person happens to be would only produce a longer overrun,\nnever a more honest record.","operationId":"clock_out_route_api_floor_clock_out_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClockOutBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Clock Out Route Api Floor Clock Out Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/clock/register":{"post":{"tags":["floor-clock"],"summary":"Register Route","description":"Picks a workstation and mints the device cookie on THIS browser.\nGated `fulfillment:equipment:write` -- the build task's own instruction:\nregistering the machine a clock-in is allowed to come from is an\nequipment write, not a floor grant.","operationId":"register_route_api_floor_clock_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Register Route Api Floor Clock Register Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/clock/register/{equipment_id}/revoke":{"post":{"tags":["floor-clock"],"summary":"Revoke Route","description":"Revokes a workstation's device token without registering a\nreplacement -- a machine decommissioned or lost, per the build task's\n\"revocable by marking the equipment row\".","operationId":"revoke_route_api_floor_clock_register__equipment_id__revoke_post","parameters":[{"name":"equipment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Equipment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revoke Route Api Floor Clock Register  Equipment Id  Revoke Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/my-station":{"get":{"tags":["floor-station"],"summary":"Get My Station","description":"Where Roost believes the caller is standing -- learned from this very\nrequest's station assertion when it carries one.","operationId":"get_my_station_api_floor_my_station_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get My Station Api Floor My Station Get"}}}}}}},"/api/floor/my-station/sticker":{"post":{"tags":["floor-station"],"summary":"Post My Station Sticker","description":"The caller scanned the sticker on their workstation. Records it as\nwhere they are standing (source `sticker`) and prints anything that was\nwaiting for them there. 400 with the person's own words when what was\nscanned is not a workstation.","operationId":"post_my_station_sticker_api_floor_my_station_sticker_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StickerBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post My Station Sticker Api Floor My Station Sticker Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/day-cutoff":{"get":{"tags":["floor-day-cutoff"],"summary":"Get Day Cutoff","description":"When the floor stops sending packages out on `day`: the last scheduled person's\nend, minus the packaging still ahead at the measured pace, never later than the\nusual cutoff for the weekday (FLR-109 item 6). `source` and `derivation` say how\nthe number was reached; `packaging.provenance` says `assumed` when no pace has\nbeen measured yet. 422 for a day that is already over.","operationId":"get_day_cutoff_api_floor_day_cutoff_get","parameters":[{"name":"day","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Day"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Day Cutoff Api Floor Day Cutoff Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/done":{"post":{"tags":["floor-done"],"summary":"Post Done","description":"The caller finished their stage. Returns Rooster's answer -- the\nsentence to show (\"Go to packaging.\") and its kind -- after recording the\nreport and queueing the answer as an instruction already acknowledged by\nthe screen (a text of it would be noise).","operationId":"post_done_api_floor_done_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Done Api Floor Done Post"}}}}}}},"/api/floor/design-review":{"post":{"tags":["floor-design-review"],"summary":"File A Design Review","description":"Put one floor design question before the board, once per\n`request_key`. A replay returns the first paper with `already_filed`.\n\n⛔THE RECEIPT IS THE ROW. The response is built from what the database\nholds after the write (`_status` re-reads the decision's ballot), never\nfrom the request, so a `decision_id` in it is a decision that exists.","operationId":"file_a_design_review_api_floor_design_review_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DesignReviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response File A Design Review Api Floor Design Review Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/design-review/{request_key}":{"get":{"tags":["floor-design-review"],"summary":"Read A Design Review","description":"Where the paper filed under `request_key` is, and what the seats have\nsaid that is theirs to show (see the module docstring for the two\nreveal gates). 404 for a key that was never filed.","operationId":"read_a_design_review_api_floor_design_review__request_key__get","parameters":[{"name":"request_key","in":"path","required":true,"schema":{"type":"string","title":"Request Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Read A Design Review Api Floor Design Review  Request Key  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/payouts/week":{"get":{"tags":["floor-payouts"],"summary":"Get Week Rota","operationId":"get_week_rota_api_floor_payouts_week_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Week Rota Api Floor Payouts Week Get"}}}}}}},"/api/floor/payouts/mine":{"get":{"tags":["floor-payouts"],"summary":"Get My Pay","description":"The caller's own pay statement. Authenticated only -- being paid is\nnot a grant somebody holds -- and keyed on the session's principal, never\non anything in the request, so there is no id to swap for somebody\nelse's.","operationId":"get_my_pay_api_floor_payouts_mine_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get My Pay Api Floor Payouts Mine Get"}}}}}}},"/api/floor/payouts/hourly-rate":{"get":{"tags":["floor-payouts"],"summary":"Get Hourly Rate","operationId":"get_hourly_rate_api_floor_payouts_hourly_rate_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Hourly Rate Api Floor Payouts Hourly Rate Get"}}}}}},"put":{"tags":["floor-payouts"],"summary":"Put Hourly Rate","description":"Frederick sets the desk's hourly rate -- behind the same signature\nthat releases the pay. Prices every pending payout that was waiting on\nit; releases nothing.","operationId":"put_hourly_rate_api_floor_payouts_hourly_rate_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HourlyRateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Put Hourly Rate Api Floor Payouts Hourly Rate Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/payouts/{payout_id}":{"get":{"tags":["floor-payouts"],"summary":"Get Payout","operationId":"get_payout_api_floor_payouts__payout_id__get","parameters":[{"name":"payout_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payout Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Payout Api Floor Payouts  Payout Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/payouts/{payout_id}/approve":{"post":{"tags":["floor-payouts"],"summary":"Approve Payout","description":"\"Approve - release via Wise\". ⛔THE ONLY PRODUCTION CALLER of this\nroute's own downstream `WisePayoutPort.fund_transfer` -- see\n`service.approve`'s own docstring.","operationId":"approve_payout_api_floor_payouts__payout_id__approve_post","parameters":[{"name":"payout_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payout Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Approve Payout Api Floor Payouts  Payout Id  Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/payouts/approve-day":{"post":{"tags":["floor-payouts"],"summary":"Approve Day","description":"\"Approve all\" on the Today day card: `service.approve` once for each\npayout the card listed, in turn (`day_batch.approve_each`). ⛔NOT A SECOND\nMONEY PATH: nothing here quotes, creates or funds a transfer; the per-payout\nlock, stored transfer id and Wise idempotency key inside `service.approve`\nare what keep a payout from being paid twice, whichever screen or Slack\nbutton reaches it first. A payout whose payment fails is reported by name\nwith Wise's reason and stays pending; the others are paid. Always 200 with\none result per payout: a refusal of one is that payout's line, not the\nrequest's.","operationId":"approve_day_api_floor_payouts_approve_day_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveDayRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Approve Day Api Floor Payouts Approve Day Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/payouts/{payout_id}/hold":{"post":{"tags":["floor-payouts"],"summary":"Hold Payout","operationId":"hold_payout_api_floor_payouts__payout_id__hold_post","parameters":[{"name":"payout_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payout Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Hold Payout Api Floor Payouts  Payout Id  Hold Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/floor/payouts/{payout_id}/flag":{"post":{"tags":["floor-payouts"],"summary":"Flag Payout","operationId":"flag_payout_api_floor_payouts__payout_id__flag_post","parameters":[{"name":"payout_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Payout Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Flag Payout Api Floor Payouts  Payout Id  Flag Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/company":{"get":{"tags":["company"],"summary":"Get Company","description":"The financial picture every employee sees, identically.\n\n⛔AUTHENTICATED, BUT NOT GATED BY ROLE. Design brief 02 §8 lists\n\"role-gated variants of the numbers\" among the things not to build:\n\"Everyone sees the same page.\" A grant here would quietly create the\nvariants the brief forbids, so there is only the ordinary signed-in check.\n\nONE NAMED EXCEPTION, since 2026-09-06: `headline.commissions_paid` (a\ncompany-wide commission-payout total) is withheld from anyone who does\nnot hold `sees_all_commissions` -- Frederick, the same day: \"The\ncommission table should only allow the employees to see their own\ncommission, so Keith can't see the others'.\" A team small enough that\nthis page is basically the whole sales org lets that one aggregate line\nstand in for an individual's number, which is exactly the inference the\nCommissions page itself now refuses to allow -- see\napp/modules/finance/commission_scope.py. Nothing else on this page is\ngated; `service.build` still hides only this one figure, never the\nblended `cost`/`monthly_cost` totals it feeds into.\n\nWhat keeps the REST of it publishable is §6, and that lives in the\nSHAPE of what is returned rather than in who may ask: no individual\ninvoice, customer, transaction or line item, nobody's pay, no cash on\nhand, and nothing naming which customer is late. See service.AGING_SQL,\nwhich selects no customer identifier at all.","operationId":"get_company_api_company_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Company Api Company Get"}}}}}}},"/api/public/investor/unlock":{"post":{"tags":["investor"],"summary":"Unlock","description":"One attempt at the keypad.\n\nReturns 200 for a refusal rather than 401: the page renders the remaining\ntries and the shake itself, and an error status would make a wrong code\nlook like a broken page to anything watching.","operationId":"unlock_api_public_investor_unlock_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Unlock Api Public Investor Unlock Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/investor/pipeline":{"get":{"tags":["investor"],"summary":"Pipeline","description":"The pipeline, as a named investor sees it.\n\n⛔THE VIEWER ID IS NOT A SECRET AND IS NOT TREATED AS ONE. It says which\nnamed viewer is reading, for nothing more than the header greeting; every\ninvestor sees the SAME figures, so a guessed id reveals nothing a correct\none would not. What keeps this endpoint safe is that it cannot return a\ncontact, an owner, a note or an invoice for anybody at all -- see\nservice.DEALS_SQL, and §6: \"the API response is the boundary\".","operationId":"pipeline_api_public_investor_pipeline_get","parameters":[{"name":"viewer_id","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Viewer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Pipeline Api Public Investor Pipeline Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/investor/viewers":{"get":{"tags":["investor"],"summary":"List Viewers","description":"Who may see the pipeline, and when they last looked.\n\n⛔THE CODE IS NEVER RETURNED. An admin who needs it can read it off the\nperson's phone number, which is where it comes from; a list endpoint that\nhands back every access code is a credential dump waiting to be pasted\nsomewhere.","operationId":"list_viewers_api_investor_viewers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Viewers Api Investor Viewers Get"}}}}}},"post":{"tags":["investor"],"summary":"Add Viewer","operationId":"add_viewer_api_investor_viewers_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ViewerIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Add Viewer Api Investor Viewers Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/investor/preview":{"get":{"tags":["investor"],"summary":"Preview","description":"The figures the investor page opens with, for the team's own preview.\n\nWHY (2026-10-04, #3583, the design conformance rebuild of Sales >\nInvestor view): `Sales.dc.html`'s Investor view draws three tiles, Total\npotential revenue, Agencies in play and Investors with access. The first\ntwo are what an investor sees at the top of `/pipeline`, and the only\nread that served them was `GET /api/public/investor/pipeline`, which\nneeds an investor's viewer id. So the team's preview could draw only the\nthird tile.\n\nThe SAME `service.build` the public page reads, so the preview can never\ndisagree with what investors are shown, and only its totals: no rows,\nno names. Gated like every other route on this router (`ADMIN`, the\nowner-level key that already decides who may let an outsider in) --\nthe page that reads it already needs it for the viewer list beside it,\nso no new authority is decided here.","operationId":"preview_api_investor_preview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Preview Api Investor Preview Get"}}}}}}},"/api/investor/viewers/{viewer_id}/revoke":{"post":{"tags":["investor"],"summary":"Revoke Viewer","description":"Take access away, keeping the record that it existed.","operationId":"revoke_viewer_api_investor_viewers__viewer_id__revoke_post","parameters":[{"name":"viewer_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Viewer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Revoke Viewer Api Investor Viewers  Viewer Id  Revoke Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/health":{"get":{"summary":"Health","description":"Readiness. 503 until migrations have run and the app is serving, so a\nrolling deploy never cuts traffic to a half-ready container.","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/health/env":{"get":{"summary":"Health Env","description":"Which environment this is (`production`, `staging`, ...), and nothing\nelse: no database, no probes. Unauthenticated, like `/api/health`.\n\nWHY IT EXISTS (#3504, Frederick 2026-10-03: \"pages are starting to load\nslow now ... Making sure things are wired up in the most efficient\nway\"). The staging banner (`apps/web/src/components/shell/\nStagingBanner.tsx`) mounts on every page load and needs only `env`, but\nit fetched the full `/api/health`, which probes every adapter and runs\nseveral queries: measured from real browsers on 2026-10-03, p50 879 ms,\np95 4,484 ms, holding a request-pool connection, once per page load.\n`/api/health` itself is unchanged (the System page and Railway's\nhealth check read it); this is the one field the banner needs, read\nfrom the same `settings.env` `/api/health` reports.","operationId":"health_env_api_health_env_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Env Api Health Env Get"}}}}}}},"/api/health/build-floor":{"get":{"summary":"Build Floor","description":"Is a builder holding an issue right now -- unauthenticated, like\n`/api/health` itself, because the one caller that needs this from\noutside the process has no bearer token to send.\n\nWHY THIS EXISTS. `scripts/promote-to-production.sh` redeploys rooster\n-- the Railway container running `tools/rooster_worker/drain.py`'s\n`hands` loop -- on every promotion, with nothing asking first whether a\nbuild is still checked out inside it. `drain.py`'s own\n`ROOSTER_DEPLOY_DEBT_CEILING` already refuses to deploy THROUGH a build\nit drives itself (its own comment: \"SO ROOSTER'S OWN DEPLOY WAITS FOR\nTHE FLOOR TO BE EMPTY... Drain then deploy -- never deploy through a\nbuild\"), but that guard runs INSIDE the container and has no way to\nanswer a question asked from outside it -- a shell script on a\ndifferent machine cannot read another process's memory\n(`drain.HeldClaims`). This is that question, asked of the database\nboth sides already agree on.\n\n`issues_service.builds_in_flight` is the exact database mirror of what\n`HeldClaims` tracks in-process, so the answer here and the answer\n`drain.py` would give itself are the same fact read from two places,\nnever two different guesses at it.\n\nTHE CEILING RIDES ALONG rather than being copied into the shell script\nas a second literal -- `ROOSTER_DEPLOY_DEBT_CEILING` is the one place\nthat number is decided (this repo's own \"one fact, many literals\ndrift\" rule), so a caller that needs it reads it off this response\ninstead of hardcoding it a second time somewhere that could drift out\nof step.\n\n⛔ITS OWN CONNECTION, NEVER THE REQUEST POOL (ISS-1565). Its one\nstatement takes 4 ms, but the route measured p95 13 s, max 38 s, with\n1,328 ms of pool wait per request: it queued behind every other request\nfor one of the pool's few slots. A short-lived connection with a connect\nand statement limit answers in milliseconds or fails fast, the same shape\n`/api/health`'s own reads use. A failure is a 503, never a guessed\n`in_flight`: a stale \"0\" would let a promotion deploy through a build.","operationId":"build_floor_api_health_build_floor_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Build Floor Api Health Build Floor Get"}}}}}}}},"components":{"schemas":{"AcceptIn":{"properties":{"category":{"type":"string","title":"Category"},"due_day":{"anyOf":[{"type":"integer","maximum":31.0,"minimum":1.0},{"type":"null"}],"title":"Due Day"}},"type":"object","required":["category"],"title":"AcceptIn"},"AcceptProposalRequest":{"properties":{"keep_org_id":{"type":"string","format":"uuid","title":"Keep Org Id"},"force_financial_merge":{"type":"boolean","title":"Force Financial Merge","default":false},"force_direction":{"type":"boolean","title":"Force Direction","default":false},"override_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Override Name"}},"type":"object","required":["keep_org_id"],"title":"AcceptProposalRequest"},"AcknowledgeIn":{"properties":{"answer":{"type":"string","minLength":1,"title":"Answer"},"note":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Note"}},"type":"object","required":["answer"],"title":"AcknowledgeIn"},"ActBody":{"properties":{"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"},"call_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Call Id"},"request_id":{"type":"string","format":"uuid","title":"Request Id"},"suggestion_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Suggestion Id"},"contact_reason":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Contact Reason"},"kind":{"type":"string","maxLength":40,"minLength":1,"title":"Kind"},"reason":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Reason"},"fields":{"additionalProperties":true,"type":"object","title":"Fields"}},"type":"object","required":["request_id","kind"],"title":"ActBody","description":"Any registered act that has no route of its own: its kind, the reason when the\ndesign asks for one, and the act's own fields (checked against the kind by\n`acts.perform`, which refuses a field the kind does not have).\n\n`request_id` is REQUIRED here, unlike on the older routes (Opus review of #4067,\n2026-10-07): it is the act ledger's idempotency key, and these two doors take money acts\nwhose double tap must be refused. A caller that omitted it was given a fresh id by\n`acts.perform` and so had no protection at all."},"ActivatePlaybookRequest":{"properties":{"allocation_weight":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Allocation Weight"}},"type":"object","title":"ActivatePlaybookRequest"},"AddFundsBody":{"properties":{"amount":{"type":"number","title":"Amount"},"confirm":{"type":"boolean","title":"Confirm","default":false}},"type":"object","required":["amount"],"title":"AddFundsBody"},"AddQuoteRequest":{"properties":{"part_number":{"type":"string","title":"Part Number"},"supplier_id":{"type":"string","format":"uuid","title":"Supplier Id"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"price_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Cents"},"condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condition"},"in_stock":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"In Stock"},"compatibility_claim":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compatibility Claim"},"machine_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Machine Model"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["part_number","supplier_id"],"title":"AddQuoteRequest"},"AddSupplierRequest":{"properties":{"name":{"type":"string","title":"Name"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"},"kind":{"type":"string","title":"Kind"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"preferred":{"type":"boolean","title":"Preferred","default":false}},"type":"object","required":["name","kind"],"title":"AddSupplierRequest"},"Address":{"properties":{"line_one":{"type":"string","maxLength":200,"title":"Line One","default":""},"line_two":{"type":"string","maxLength":200,"title":"Line Two","default":""},"city":{"type":"string","maxLength":120,"title":"City","default":""},"governing_district":{"type":"string","maxLength":80,"title":"Governing District","default":""},"postal_code":{"type":"string","maxLength":20,"title":"Postal Code","default":""}},"type":"object","title":"Address","description":"As written on the envelope, not as corrected.\n\n⛔NOT VALIDATED AGAINST USPS, DELIBERATELY. This is a transcription of\nwhat the customer wrote, and it is useful precisely because it is\nverbatim: fulfilment matches the physical envelope against it, and the\nreturn address AS WRITTEN is what decides whether it comes back at all. A\n\"corrected\" address would break both jobs and hide the reason a return\nfailed."},"AddressToCheck":{"properties":{"street":{"type":"string","title":"Street"},"secondary":{"type":"string","title":"Secondary","default":""},"city":{"type":"string","title":"City"},"state":{"type":"string","title":"State"},"zip_code":{"type":"string","title":"Zip Code","default":""}},"type":"object","required":["street","city","state"],"title":"AddressToCheck"},"AddressUpdateRequest":{"properties":{"new_address":{"type":"string","title":"New Address"}},"type":"object","required":["new_address"],"title":"AddressUpdateRequest"},"AdvanceInstructionRequest":{"properties":{"status":{"type":"string","title":"Status"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["status"],"title":"AdvanceInstructionRequest"},"AgentBrainReport":{"properties":{"provider":{"type":"string","title":"Provider"},"model":{"type":"string","title":"Model"}},"type":"object","required":["provider","model"],"title":"AgentBrainReport","description":"What Goose actually resolved this boot. Reported BY the agent, because\nthe API cannot see the container's GOOSE_PROVIDER/GOOSE_MODEL and any\nserver-side guess would be config dressed as fact -- which is exactly what\nthe System page's model panel was doing."},"AgentDirectionRequest":{"properties":{"principal_id":{"type":"string","format":"uuid","title":"Principal Id"},"text":{"type":"string","title":"Text"},"for_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"For Date"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"}},"type":"object","required":["principal_id","text"],"title":"AgentDirectionRequest"},"AgentDirectiveRequest":{"properties":{"kinds":{"items":{"type":"string"},"type":"array","title":"Kinds"},"text":{"type":"string","title":"Text"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"}},"type":"object","required":["kinds","text"],"title":"AgentDirectiveRequest"},"AgentDraftRequest":{"properties":{"text":{"type":"string","title":"Text"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"},"agent_harness":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Harness"},"proposed_status":{"anyOf":[{"type":"string","enum":["open","waiting_on_customer","resolved"]},{"type":"null"}],"title":"Proposed Status"},"proposed_status_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Status Reason"}},"type":"object","required":["text"],"title":"AgentDraftRequest"},"AgentNotInterestedRequest":{"properties":{"rationale":{"type":"string","title":"Rationale"},"confidence":{"type":"number","title":"Confidence"},"scope":{"type":"string","title":"Scope","default":"domain"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"}},"type":"object","required":["rationale","confidence"],"title":"AgentNotInterestedRequest","description":"Rooster's verdict on a piece of mail.\n\nHe supplies the JUDGMENT -- is this bulk marketing, how sure is he, and\nwhy. Everything else (closing the ticket, writing the rule, logging the\nact, scoring the call) is this service's job, per the deterministic-spine\nrule."},"AnswerBuildQuestionRequest":{"properties":{"answer":{"type":"string","title":"Answer"}},"type":"object","required":["answer"],"title":"AnswerBuildQuestionRequest"},"AnswerHereResult":{"properties":{"answered":{"type":"boolean","title":"Answered"},"said":{"type":"string","title":"Said"}},"type":"object","required":["answered","said"],"title":"AnswerHereResult"},"AnswerIn":{"properties":{"answer":{"additionalProperties":true,"type":"object","title":"Answer"},"run":{"$ref":"#/components/schemas/RunIn"}},"type":"object","required":["answer","run"],"title":"AnswerIn"},"AnswerQuestionRequest":{"properties":{"reply":{"type":"string","title":"Reply"},"evidence":{"items":{"type":"string"},"type":"array","title":"Evidence","default":[]},"what_you_can_do":{"items":{"type":"string"},"type":"array","title":"What You Can Do","default":[]},"offer_to_file":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Offer To File"},"confirms_pending_offer":{"type":"boolean","title":"Confirms Pending Offer","default":false},"model":{"type":"string","title":"Model"},"provider":{"type":"string","title":"Provider","default":"claude_code_seat"},"tokens_in":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tokens In"},"tokens_out":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tokens Out"},"cost_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost Usd"}},"type":"object","required":["reply","model"],"title":"AnswerQuestionRequest"},"AnswerRequest":{"properties":{"verdict":{"type":"string","enum":["business","personal"],"title":"Verdict"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"note":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Note"}},"type":"object","required":["verdict"],"title":"AnswerRequest","description":"Frederick's answer to one flag. `category` is optional (Rooster's guess is\nkept when he names none) and must be on the closed list."},"AnswerWindowRequest":{"properties":{"hours":{"type":"integer","title":"Hours"}},"type":"object","required":["hours"],"title":"AnswerWindowRequest","description":"How long the board gets before silence becomes `expired`."},"AppendDealEventRequest":{"properties":{"event_type":{"type":"string","title":"Event Type"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"}},"type":"object","required":["event_type"],"title":"AppendDealEventRequest"},"AppendLeadEventRequest":{"properties":{"event_type":{"type":"string","title":"Event Type"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"}},"type":"object","required":["event_type"],"title":"AppendLeadEventRequest"},"AppendOrgContactEventRequest":{"properties":{"event_type":{"type":"string","title":"Event Type"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"}},"type":"object","required":["event_type"],"title":"AppendOrgContactEventRequest","description":"No `actor_kind` field, deliberately. Every write through this route\nis a `human` event carrying the AUTHENTICATED caller's own principal id\n(see the route below) -- a request body has no way to ask for a\n'rooster' row, so nothing arriving over HTTP can forge Rooster\nattribution. See app/modules/orgs/service.py append_org_contact_event's\nown docstring."},"ApproveDayRequest":{"properties":{"payout_ids":{"items":{"type":"string","format":"uuid"},"type":"array","maxItems":50,"minItems":1,"title":"Payout Ids"}},"type":"object","required":["payout_ids"],"title":"ApproveDayRequest"},"ApproveStoryRequest":{"properties":{"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","title":"ApproveStoryRequest"},"AskRequest":{"properties":{"question":{"type":"string","title":"Question"}},"type":"object","required":["question"],"title":"AskRequest"},"AskResponse":{"properties":{"answer":{"type":"string","title":"Answer"},"citations":{"items":{"$ref":"#/components/schemas/CitationOut"},"type":"array","title":"Citations"},"refused":{"anyOf":[{"$ref":"#/components/schemas/RefusalOut"},{"type":"null"}]}},"type":"object","required":["answer","citations"],"title":"AskResponse"},"AssignOwnerRequest":{"properties":{"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"}},"type":"object","title":"AssignOwnerRequest"},"AssignRoleRequest":{"properties":{"role_id":{"type":"string","format":"uuid","title":"Role Id"}},"type":"object","required":["role_id"],"title":"AssignRoleRequest"},"AssignTicketRequest":{"properties":{"assignee_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Assignee Principal Id"}},"type":"object","title":"AssignTicketRequest","description":"The desk's manual reassignment control (Frederick, 2026-07-30:\n\"assignees can be changed ... I should be able to assign it to another\nteam person\"). `None` hands the ticket back to Unassigned -- a real,\nreachable state (see UNASSIGNED_OWNER), not a value this endpoint\nrefuses."},"AttendanceBody":{"properties":{"always_attends":{"type":"boolean","title":"Always Attends","default":false},"constrains_availability":{"type":"boolean","title":"Constrains Availability","default":false}},"type":"object","title":"AttendanceBody","description":"Somebody's role on OTHER people's demos -- see service.set_attendance."},"AuthBody":{"properties":{"password":{"type":"string","maxLength":200,"minLength":1,"title":"Password"},"booker_name":{"type":"string","maxLength":200,"title":"Booker Name","default":""},"booker_email":{"type":"string","maxLength":320,"title":"Booker Email","default":""}},"type":"object","required":["password"],"title":"AuthBody","description":"The password, and who is behind it.\n\nThe name and email are OPTIONAL AT THE WIRE and required by the page. A\nsession opened without them still works -- it just books meetings with\nnobody from Locke on the invite, which is what every booking did before\n2026-08-21 and is a worse outcome, not a broken one."},"AvailabilityBody":{"properties":{"timezone":{"type":"string","title":"Timezone"},"start_minute":{"type":"integer","maximum":1439.0,"minimum":0.0,"title":"Start Minute"},"end_minute":{"type":"integer","maximum":1440.0,"minimum":1.0,"title":"End Minute"},"weekdays":{"items":{"type":"integer"},"type":"array","title":"Weekdays"},"slot_minutes":{"type":"integer","maximum":480.0,"minimum":5.0,"title":"Slot Minutes","default":30},"buffer_minutes":{"type":"integer","maximum":240.0,"minimum":0.0,"title":"Buffer Minutes","default":30},"horizon_days":{"type":"integer","maximum":120.0,"minimum":1.0,"title":"Horizon Days","default":21},"accepting":{"type":"boolean","title":"Accepting","default":false},"invitable":{"type":"boolean","title":"Invitable","default":false},"blackouts":{"items":{"type":"string","format":"date"},"type":"array","title":"Blackouts"}},"type":"object","required":["timezone","start_minute","end_minute","weekdays"],"title":"AvailabilityBody"},"AwaitingConfirmationRequest":{"properties":{"after_submitted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"After Submitted At"},"after_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"After Id"},"limit":{"type":"integer","minimum":1.0,"title":"Limit","default":500}},"type":"object","title":"AwaitingConfirmationRequest","description":"Which page to read: the first when both cursor fields are absent (an\nagent older than 1.5 sends no body at all and gets the first page)."},"AwaitingConfirmationResponse":{"properties":{"jobs":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Jobs"},"next":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Next"}},"type":"object","required":["jobs"],"title":"AwaitingConfirmationResponse"},"BankMatchIn":{"properties":{"transaction_id":{"type":"string","title":"Transaction Id"}},"type":"object","required":["transaction_id"],"title":"BankMatchIn"},"BillIn":{"properties":{"payee":{"type":"string","maxLength":160,"minLength":1,"title":"Payee"},"category":{"type":"string","title":"Category"},"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Amount"},"cadence":{"type":"string","title":"Cadence"},"start_date":{"type":"string","format":"date","title":"Start Date"},"due_day":{"anyOf":[{"type":"integer","maximum":31.0,"minimum":1.0},{"type":"null"}],"title":"Due Day"},"opening_balance":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Opening Balance","default":0},"opening_balance_as_of":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Opening Balance As Of"},"note":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Note"},"person_name":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Person Name"},"person_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Person Principal Id"},"match_terms":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Match Terms"},"in_run_rate":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"In Run Rate"},"kind":{"type":"string","enum":["fixed_recurring","invoiced_variable","usage_reload"],"title":"Kind","default":"fixed_recurring"},"balance_source":{"anyOf":[{"type":"string","const":"stamps"},{"type":"null"}],"title":"Balance Source"},"confirm_duplicate":{"type":"boolean","title":"Confirm Duplicate","default":false}},"type":"object","required":["payee","category","amount","cadence","start_date"],"title":"BillIn"},"BindBoxBarcodeRequest":{"properties":{"raw":{"type":"string","title":"Raw"},"packaging_key":{"type":"string","title":"Packaging Key"}},"type":"object","required":["raw","packaging_key"],"title":"BindBoxBarcodeRequest"},"BindPackagingBarcodeRequest":{"properties":{"upc":{"type":"string","title":"Upc"}},"type":"object","required":["upc"],"title":"BindPackagingBarcodeRequest"},"Body_apply_paperwork_route_api_equipment__equipment_id__paperwork_post":{"properties":{"file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"File"}},"type":"object","title":"Body_apply_paperwork_route_api_equipment__equipment_id__paperwork_post"},"Body_attach_to_intake_api_issues_intake_attach_post":{"properties":{"files":{"items":{"type":"string","format":"binary"},"type":"array","title":"Files"},"conversation_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Conversation Id"}},"type":"object","required":["files"],"title":"Body_attach_to_intake_api_issues_intake_attach_post"},"Body_import_stamps_balances_trend_api_finance_company_numbers_stamps_import_balances_trend_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_import_stamps_balances_trend_api_finance_company_numbers_stamps_import_balances_trend_post"},"Body_preview_document_template_api_sales_document_templates_preview_post":{"properties":{"kind":{"type":"string","title":"Kind"},"title":{"type":"string","maxLength":300,"title":"Title"},"body":{"type":"string","title":"Body","default":""},"questions":{"type":"string","title":"Questions","default":"[]"},"choices":{"type":"string","title":"Choices","default":"[]"},"org_id":{"type":"string","format":"uuid","title":"Org Id"},"picked":{"type":"string","title":"Picked","default":"{}"},"keep_live_source":{"type":"boolean","title":"Keep Live Source","default":false},"file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"File"}},"type":"object","required":["kind","title","org_id"],"title":"Body_preview_document_template_api_sales_document_templates_preview_post"},"Body_publish_document_template_api_sales_document_templates_post":{"properties":{"kind":{"type":"string","title":"Kind"},"title":{"type":"string","maxLength":300,"title":"Title"},"body":{"type":"string","title":"Body","default":""},"questions":{"type":"string","title":"Questions","default":"[]"},"choices":{"type":"string","title":"Choices","default":"[]"},"changelog":{"type":"string","maxLength":1000,"title":"Changelog"},"bump":{"type":"string","title":"Bump","default":"minor"},"keep_live_source":{"type":"boolean","title":"Keep Live Source","default":false},"file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"File"}},"type":"object","required":["kind","title","changelog"],"title":"Body_publish_document_template_api_sales_document_templates_post"},"Body_read_paperwork_prefill_route_api_equipment_paperwork_read_post":{"properties":{"file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"File"},"manufacturer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manufacturer"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"}},"type":"object","title":"Body_read_paperwork_prefill_route_api_equipment_paperwork_read_post"},"Body_record_a_menu_line_api_voice_prompts__key__recording_put":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_record_a_menu_line_api_voice_prompts__key__recording_put"},"Body_service_document_route_api_equipment__equipment_id__service_document_post":{"properties":{"file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"File"}},"type":"object","title":"Body_service_document_route_api_equipment__equipment_id__service_document_post"},"Body_upload_message_attachments_api_cs_tickets__ticket_id__messages__message_id__attachments_post":{"properties":{"files":{"items":{"type":"string","format":"binary"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_upload_message_attachments_api_cs_tickets__ticket_id__messages__message_id__attachments_post"},"BookBody":{"properties":{"purpose":{"type":"string","title":"Purpose"},"organization_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Organization Id"},"organization_name":{"type":"string","maxLength":300,"minLength":1,"title":"Organization Name"},"contact_name":{"type":"string","maxLength":200,"minLength":1,"title":"Contact Name"},"contact_title":{"type":"string","maxLength":200,"title":"Contact Title","default":""},"contact_email":{"type":"string","maxLength":320,"title":"Contact Email"},"contact_phone":{"type":"string","maxLength":40,"title":"Contact Phone","default":""},"committee":{"type":"string","maxLength":200,"title":"Committee","default":""},"target_org_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Target Org Ids"},"target_org_names":{"items":{"type":"string"},"type":"array","title":"Target Org Names"},"host_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Host Ids"},"optional_host_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Optional Host Ids"},"guests":{"items":{"$ref":"#/components/schemas/GuestBody"},"type":"array","title":"Guests"},"tz":{"type":"string","maxLength":100,"minLength":1,"title":"Tz"},"slot_start_utc":{"type":"string","format":"date-time","title":"Slot Start Utc"},"their_meeting_link":{"type":"string","maxLength":4000,"title":"Their Meeting Link","default":""},"their_location":{"type":"string","maxLength":300,"title":"Their Location","default":""}},"type":"object","required":["purpose","organization_name","contact_name","contact_email","tz","slot_start_utc"],"title":"BookBody"},"BookHuddleRequest":{"properties":{"requester_principal_id":{"type":"string","format":"uuid","title":"Requester Principal Id"},"target_principal_id":{"type":"string","format":"uuid","title":"Target Principal Id"},"item_description":{"type":"string","title":"Item Description"},"duration_minutes":{"type":"integer","maximum":120.0,"minimum":5.0,"title":"Duration Minutes","default":15}},"type":"object","required":["requester_principal_id","target_principal_id","item_description"],"title":"BookHuddleRequest"},"BoxAnswer":{"properties":{"parcel_code":{"type":"string","title":"Parcel Code"},"box_label_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Box Label Id"},"problem":{"type":"string","title":"Problem","default":""}},"type":"object","required":["parcel_code"],"title":"BoxAnswer","description":"What a parcel code resolves to, or why it did not."},"BridgeCallCallerRequest":{"properties":{"call_uuid":{"type":"string","title":"Call Uuid"},"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},"type":"object","required":["call_uuid"],"title":"BridgeCallCallerRequest","description":"Who placed a cell-bridge call, self-reported by the browser."},"BrowserCallAnsweredRequest":{"properties":{"call_uuid":{"type":"string","title":"Call Uuid"},"to_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To Number"},"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},"type":"object","required":["call_uuid"],"title":"BrowserCallAnsweredRequest","description":"The softphone saying which call it is on, the moment it is answered."},"BrowserTokenResponse":{"properties":{"configured":{"type":"boolean","title":"Configured"},"token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["configured","token","expires_at"],"title":"BrowserTokenResponse"},"BuildModelRequest":{"properties":{"model":{"type":"string","title":"Model"}},"type":"object","required":["model"],"title":"BuildModelRequest"},"BuildNeedsDecisionRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"BuildNeedsDecisionRequest"},"BuildQuestionRequest":{"properties":{"question":{"type":"string","title":"Question"},"needs_human_hands":{"type":"boolean","title":"Needs Human Hands","default":false}},"type":"object","required":["question"],"title":"BuildQuestionRequest"},"BuildUnreachableRequest":{"properties":{"detail":{"type":"string","title":"Detail","default":""},"pr_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Url"},"saved_work":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Saved Work"}},"type":"object","title":"BuildUnreachableRequest"},"BuildUpstreamRequest":{"properties":{"service":{"type":"string","title":"Service"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["service","reason"],"title":"BuildUpstreamRequest"},"BuilderCredentialReport":{"properties":{"ok":{"type":"boolean","title":"Ok"},"credential":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Credential"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"limited":{"type":"boolean","title":"Limited","default":false}},"type":"object","required":["ok"],"title":"BuilderCredentialReport"},"BulkAcceptItemRequest":{"properties":{"proposal_id":{"type":"string","format":"uuid","title":"Proposal Id"},"keep_org_id":{"type":"string","format":"uuid","title":"Keep Org Id"},"force_financial_merge":{"type":"boolean","title":"Force Financial Merge","default":false},"force_direction":{"type":"boolean","title":"Force Direction","default":false},"override_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Override Name"}},"type":"object","required":["proposal_id","keep_org_id"],"title":"BulkAcceptItemRequest"},"BulkAcceptRequest":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BulkAcceptItemRequest"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"BulkAcceptRequest"},"BulkRejectRequest":{"properties":{"proposal_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Proposal Ids"}},"type":"object","required":["proposal_ids"],"title":"BulkRejectRequest"},"BulkTransitionRequest":{"properties":{"ticket_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Ticket Ids"},"to_status":{"type":"string","title":"To Status"}},"type":"object","required":["ticket_ids","to_status"],"title":"BulkTransitionRequest","description":"The clean-slate desk's bulk action (Frederick, 2026-08-09: \"bulk\nactions, including resolve WITHOUT emailing\"). One status for every\nticket in `ticket_ids` -- there is no per-ticket status in this request,\nmatching the single-ticket TransitionRequest's own shape (one status,\napplied through the SAME service.transition_ticket)."},"CallAttemptsResponse":{"properties":{"ticket_id":{"type":"string","format":"uuid","title":"Ticket Id"},"attempts":{"type":"integer","title":"Attempts"},"max_attempts":{"type":"integer","title":"Max Attempts"},"next_attempt_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Next Attempt At"},"unable_to_reach_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unable To Reach Summary"}},"type":"object","required":["ticket_id","attempts","max_attempts","next_attempt_at"],"title":"CallAttemptsResponse","description":"One ticket's calls, as a screen shows them: \"Attempt N of 3\" and when\nthe next try is due."},"CallOutBody":{"properties":{"sick":{"type":"boolean","title":"Sick","default":true},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","title":"CallOutBody"},"CallOutRequest":{"properties":{"day":{"type":"string","format":"date","title":"Day"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["day"],"title":"CallOutRequest"},"CallbackHoursResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"next_allowed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Allowed At"},"customer_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Zone"}},"type":"object","required":["ok"],"title":"CallbackHoursResponse","description":"Whether a ticket's callback may be dialled NOW, asked BEFORE dialling.\n\n`ok` true: go ahead. `ok` false: the pop-up's facts -- same fields as\n`OutsideCallingHoursBody`, so the browser draws one dialog for both."},"CallerMatchResponse":{"properties":{"kind":{"type":"string","title":"Kind"},"org_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Org Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"caveat":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caveat"}},"type":"object","required":["kind","org_id","contact_id","caveat"],"title":"CallerMatchResponse"},"CancelBody":{"properties":{"reason":{"type":"string","maxLength":1000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"CancelBody"},"CancelDemoRequest":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Reason"},"remove_created_records":{"type":"boolean","title":"Remove Created Records","default":false}},"type":"object","title":"CancelDemoRequest","description":"Why it is being cancelled. Optional, because a person looking at an\nobvious spam booking should not have to write an essay to get rid of it --\nbut recorded when given, since \"spam\" appearing forty times is how you find\nout the demo page needs a harder gate."},"CandidatesRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"tiers":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tiers"},"limit_per_tier":{"type":"integer","title":"Limit Per Tier","default":25},"minimum_accuracy":{"type":"integer","title":"Minimum Accuracy","default":85}},"type":"object","required":["org_id"],"title":"CandidatesRequest"},"CaseResearchSayRequest":{"properties":{"body":{"type":"string","maxLength":8000,"minLength":1,"title":"Body"}},"type":"object","required":["body"],"title":"CaseResearchSayRequest"},"CellBridgeRequest":{"properties":{"to_number":{"type":"string","title":"To Number"},"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},"type":"object","required":["to_number"],"title":"CellBridgeRequest"},"CellBridgeResult":{"properties":{"call_uuid":{"type":"string","title":"Call Uuid"},"customer_number_ends":{"type":"string","title":"Customer Number Ends"},"caller_id":{"type":"string","title":"Caller Id"}},"type":"object","required":["call_uuid","customer_number_ends","caller_id"],"title":"CellBridgeResult"},"CertifyBody":{"properties":{"step":{"type":"string","title":"Step"},"attested_trained":{"type":"boolean","title":"Attested Trained","default":false},"attested_ran_alongside":{"type":"boolean","title":"Attested Ran Alongside","default":false},"attested_alone":{"type":"boolean","title":"Attested Alone","default":false}},"type":"object","required":["step"],"title":"CertifyBody"},"ChairRulingRequest":{"properties":{"chosen_option":{"type":"string","title":"Chosen Option"},"rationale":{"type":"string","title":"Rationale"}},"type":"object","required":["chosen_option","rationale"],"title":"ChairRulingRequest","description":"The chair breaks it."},"ChatwootConversationEvent":{"properties":{"event":{"type":"string","title":"Event"},"conversation":{"additionalProperties":true,"type":"object","title":"Conversation"}},"type":"object","required":["event","conversation"],"title":"ChatwootConversationEvent"},"ChatwootMessageEvent":{"properties":{"event":{"type":"string","title":"Event"},"message":{"additionalProperties":true,"type":"object","title":"Message"},"raw":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Raw"},"conversation":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Conversation"}},"type":"object","required":["event","message"],"title":"ChatwootMessageEvent"},"CitationOut":{"properties":{"fact":{"type":"string","title":"Fact"},"source":{"type":"string","title":"Source"},"receipt":{"type":"string","title":"Receipt"}},"type":"object","required":["fact","source","receipt"],"title":"CitationOut"},"ClaimDraftReviewRequest":{"properties":{"pr_number":{"type":"integer","title":"Pr Number"},"head_sha":{"type":"string","title":"Head Sha"}},"type":"object","required":["pr_number","head_sha"],"title":"ClaimDraftReviewRequest"},"ClaimPrReviewRequest":{"properties":{"pr_number":{"type":"integer","title":"Pr Number"},"head_sha":{"type":"string","title":"Head Sha"},"seat":{"type":"string","title":"Seat"}},"type":"object","required":["pr_number","head_sha","seat"],"title":"ClaimPrReviewRequest"},"ClaimQuestionRequest":{"properties":{"worker":{"type":"string","title":"Worker"}},"type":"object","required":["worker"],"title":"ClaimQuestionRequest"},"ClaimResponse":{"properties":{"job":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Job"}},"type":"object","required":["job"],"title":"ClaimResponse"},"ClockInBody":{"properties":{"block_id":{"type":"string","format":"uuid","title":"Block Id"},"tubs":{"anyOf":[{"$ref":"#/components/schemas/TubsBody"},{"type":"null"}]}},"type":"object","required":["block_id"],"title":"ClockInBody"},"ClockInRequest":{"properties":{"block_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Block Id"},"tubs":{"anyOf":[{"$ref":"#/components/schemas/TubsBody"},{"type":"null"}]}},"type":"object","title":"ClockInRequest","description":"Optional. The Today bar posts nothing; the floor's own door names the\nblock and the day's tub count, and so may anything else that knows them.\nEither way the SAME function decides (`service.clock_in`)."},"ClockOutBody":{"properties":{"short_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Reason"}},"type":"object","title":"ClockOutBody"},"ClockOutRequest":{"properties":{"break_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Break Minutes"},"short_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Reason"}},"type":"object","title":"ClockOutRequest"},"CloseAccountRequest":{"properties":{"status":{"type":"string","title":"Status"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["status","reason"],"title":"CloseAccountRequest"},"CodeIn":{"properties":{"code":{"type":"string","maxLength":8,"minLength":1,"title":"Code"}},"type":"object","required":["code"],"title":"CodeIn"},"CodexSeatLoginRequest":{"properties":{"auth_json":{"type":"string","minLength":1,"title":"Auth Json"}},"type":"object","required":["auth_json"],"title":"CodexSeatLoginRequest","description":"The `auth.json` the Codex CLI wrote. Its text is never echoed back."},"ColleagueOut":{"properties":{"principal_id":{"type":"string","title":"Principal Id"},"display_name":{"type":"string","title":"Display Name"},"on_shift":{"type":"boolean","title":"On Shift"},"forwarding_allowed":{"type":"boolean","title":"Forwarding Allowed"}},"type":"object","required":["principal_id","display_name","on_shift","forwarding_allowed"],"title":"ColleagueOut"},"CommitBody":{"properties":{"chosen_option":{"type":"string","title":"Chosen Option"},"note":{"type":"string","title":"Note"}},"type":"object","required":["chosen_option","note"],"title":"CommitBody"},"CompanySettingsIn":{"properties":{"company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Name"},"address1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address1"},"address2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"remittance_payee":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Remittance Payee"},"remittance_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Remittance Address"},"remittance_instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Remittance Instructions"},"bank_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Name"},"ach_routing":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ach Routing"},"ach_account":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ach Account"},"invoice_contact_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invoice Contact Email"},"invoice_mailing_instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invoice Mailing Instructions"}},"type":"object","title":"CompanySettingsIn","description":"PUT /api/finance/company-settings body. Every field is optional: only\nthe fields the request actually carries are changed (`exclude_unset`),\nso the screen sends a bank number ONLY when somebody typed a new one --\nthe stored one is never round-tripped through the browser to be\n\"unchanged\"."},"CompleteBody":{"properties":{"handed_to_usps_on":{"type":"string","format":"date","title":"Handed To Usps On"},"note":{"type":"string","maxLength":1000,"title":"Note","default":""},"parcel_code":{"type":"string","maxLength":60,"title":"Parcel Code","default":""},"box_label_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Box Label Id"}},"type":"object","required":["handed_to_usps_on"],"title":"CompleteBody"},"ComposeBatchesRequest":{"properties":{"facility_id":{"type":"string","format":"uuid","title":"Facility Id"},"mail_class":{"type":"string","title":"Mail Class","default":"bulk"},"product":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product"},"limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}},"type":"object","required":["facility_id"],"title":"ComposeBatchesRequest"},"ConfirmPackagingMismatchRequest":{"properties":{"raw":{"type":"string","title":"Raw"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["raw","reason"],"title":"ConfirmPackagingMismatchRequest"},"ContactRoleRequest":{"properties":{"role_tag":{"type":"string","title":"Role Tag"},"role_rank":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role Rank"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["role_tag"],"title":"ContactRoleRequest","description":"One entry of the full desired role set for a contact.\n\n`role_rank` is optional: omitting it (or sending `null`) is how a caller\nsays \"I have not named a rank\" -- required shape for every unranked tag\n(contact_roles.RANKED_ROLE_TAGS is `('billing',)` alone), and for billing\nit means \"let the system decide\" (contact_roles_service.\n_resolve_implicit_rank: a lone holder becomes primary with no question\nasked; a second live holder raises BillingRankRequired, asking the\ncaller to resubmit naming one)."},"ConvertLeadRequest":{"properties":{"path":{"type":"string","title":"Path"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"},"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"amount_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Cents"},"close_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Close Date"},"deal_size":{"type":"string","title":"Deal Size","default":"full"}},"type":"object","required":["path"],"title":"ConvertLeadRequest"},"CourtReportIn":{"properties":{"through":{"type":"string","format":"date","title":"Through"},"source":{"type":"string","maxLength":300,"minLength":1,"title":"Source"},"classes":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}]},"type":"object","title":"Classes"}},"type":"object","required":["through","source","classes"],"title":"CourtReportIn"},"CreateBlockRequest":{"properties":{"decider_principal_id":{"type":"string","format":"uuid","title":"Decider Principal Id"},"subject":{"type":"string","title":"Subject"}},"type":"object","required":["decider_principal_id","subject"],"title":"CreateBlockRequest"},"CreateCommitmentRequest":{"properties":{"owed_to_principal_id":{"type":"string","format":"uuid","title":"Owed To Principal Id"},"what":{"type":"string","title":"What"},"due_at":{"type":"string","format":"date-time","title":"Due At"},"owed_by_department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owed By Department"},"owed_to_department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owed To Department"},"settles_on":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Settles On"}},"type":"object","required":["owed_to_principal_id","what","due_at"],"title":"CreateCommitmentRequest"},"CreateContactRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"role_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role Title"},"is_champion":{"type":"boolean","title":"Is Champion","default":false},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"mailing_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mailing Address"},"is_distribution_list":{"type":"boolean","title":"Is Distribution List","default":false},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"roles":{"items":{"$ref":"#/components/schemas/ContactRoleRequest"},"type":"array","title":"Roles","default":[]}},"type":"object","required":["org_id","first_name","last_name"],"title":"CreateContactRequest"},"CreateContractRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"contract_type":{"type":"string","title":"Contract Type"},"pricing_model":{"type":"string","title":"Pricing Model"},"starts_on":{"type":"string","format":"date","title":"Starts On"},"term_no":{"type":"integer","title":"Term No","default":1},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"billing_interval":{"type":"string","title":"Billing Interval","default":"monthly"},"billing_timing":{"type":"string","title":"Billing Timing","default":"in_advance"},"payment_terms":{"type":"string","title":"Payment Terms","default":"net_30"},"payment_method":{"type":"string","title":"Payment Method","default":"check"},"billable_mail_types":{"type":"string","title":"Billable Mail Types","default":"physical_only"},"status":{"type":"string","title":"Status","default":"active"},"adp_count":{"type":"integer","title":"Adp Count","default":0},"rate_per_adp":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Rate Per Adp"},"monthly_minimum":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Monthly Minimum"},"rate_per_parcel":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Rate Per Parcel"},"flat_amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Flat Amount"},"ends_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Ends On"},"auto_renew":{"type":"boolean","title":"Auto Renew","default":true},"sales_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Sales Principal Id"},"vp_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Vp Principal Id"},"source":{"type":"string","title":"Source","default":"manual"},"commission_from":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Commission From"},"signed_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Signed On"},"deal_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Source"},"selection_route":{"type":"string","title":"Selection Route","default":"unknown"},"payment_mechanism":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Mechanism"},"opportunity_origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opportunity Origin"},"special_commission_rate":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Special Commission Rate"}},"type":"object","required":["org_id","contract_type","pricing_model","starts_on"],"title":"CreateContractRequest"},"CreateDealRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"path":{"type":"string","title":"Path"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"},"lead_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Lead Id"},"deal_size":{"type":"string","title":"Deal Size","default":"full"},"amount_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Cents"},"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"close_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Close Date"},"unrealized_full_value_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Unrealized Full Value Cents"},"originated_by_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Originated By Principal Id"},"booked_it_myself":{"type":"boolean","title":"Booked It Myself","default":false}},"type":"object","required":["org_id","path"],"title":"CreateDealRequest"},"CreateEquipmentRequest":{"properties":{"name":{"type":"string","title":"Name"},"type":{"type":"string","title":"Type"},"step":{"type":"string","title":"Step"},"manufacturer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manufacturer"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_number":{"type":"string","title":"Serial Number"},"vendor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vendor"},"contract_level":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contract Level"},"purchase_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Purchase Cents"},"lease_cents_month":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lease Cents Month"},"in_service_at":{"type":"string","format":"date","title":"In Service At"},"rated_throughput_per_hour":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rated Throughput Per Hour"},"paperwork_dropped":{"type":"boolean","title":"Paperwork Dropped","default":false},"product_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product Type"}},"type":"object","required":["name","type","step","serial_number","in_service_at"],"title":"CreateEquipmentRequest","description":"The add-equipment form's column 1 -- README section 2, verbatim."},"CreateFacilityRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"name":{"type":"string","title":"Name"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"adp":{"type":"integer","title":"Adp","default":0},"address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address"}},"type":"object","required":["org_id","name"],"title":"CreateFacilityRequest"},"CreateGrantRequest":{"properties":{"function_key":{"type":"string","title":"Function Key"}},"type":"object","required":["function_key"],"title":"CreateGrantRequest"},"CreateInstructionRequest":{"properties":{"text":{"type":"string","maxLength":20000,"minLength":1,"title":"Text"}},"type":"object","required":["text"],"title":"CreateInstructionRequest"},"CreateInventoryItemRequest":{"properties":{"item_group":{"type":"string","title":"Item Group"},"item":{"type":"string","title":"Item"},"unit":{"type":"string","title":"Unit","default":"unit"},"equipment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Equipment"},"upc":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Upc"},"quantity_on_hand":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Quantity On Hand","default":"0"},"burn_per_day":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Burn Per Day","default":"0"},"reorder_point":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Reorder Point"},"autopilot_grant_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Autopilot Grant Key"}},"type":"object","required":["item_group","item"],"title":"CreateInventoryItemRequest"},"CreateInvoiceRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"invoice_date":{"type":"string","format":"date","title":"Invoice Date"},"due_date":{"type":"string","format":"date","title":"Due Date"},"billing_period_start":{"type":"string","format":"date","title":"Billing Period Start"},"billing_period_end":{"type":"string","format":"date","title":"Billing Period End"},"line_items":{"items":{"$ref":"#/components/schemas/LineItemBody"},"type":"array","title":"Line Items"},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"term_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Term Id"},"invoice_source":{"type":"string","title":"Invoice Source","default":"manual"},"status":{"type":"string","title":"Status","default":"draft"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["org_id","invoice_date","due_date","billing_period_start","billing_period_end","line_items"],"title":"CreateInvoiceRequest"},"CreateLeadRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"source":{"type":"string","title":"Source"},"stage":{"type":"string","title":"Stage","default":"conv"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"},"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"originated_by_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Originated By Principal Id"},"booked_it_myself":{"type":"boolean","title":"Booked It Myself","default":false}},"type":"object","required":["org_id","source"],"title":"CreateLeadRequest"},"CreateMessageRequest":{"properties":{"author_type":{"type":"string","title":"Author Type"},"body":{"type":"string","title":"Body"},"channel":{"type":"string","title":"Channel"},"direction":{"type":"string","title":"Direction"},"author_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Author Principal Id"},"author_external_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author External Name"},"state":{"type":"string","title":"State","default":"sent"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"in_reply_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"In Reply To"},"email_thread_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Thread Id"},"is_private":{"type":"boolean","title":"Is Private","default":false},"cc_emails":{"items":{"type":"string"},"type":"array","title":"Cc Emails"}},"type":"object","required":["author_type","body","channel","direction"],"title":"CreateMessageRequest"},"CreateOrganizationRequest":{"properties":{"name":{"type":"string","title":"Name"},"org_type":{"type":"string","title":"Org Type"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"jurisdiction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"lifecycle_stage":{"type":"string","title":"Lifecycle Stage","default":"prospect"},"billing_type":{"type":"string","title":"Billing Type","default":"direct"},"org_adp":{"type":"integer","title":"Org Adp","default":0},"account_manager_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Account Manager Principal Id"},"billing_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Email"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"tara_auto_send":{"type":"boolean","title":"Tara Auto Send","default":true}},"type":"object","required":["name","org_type"],"title":"CreateOrganizationRequest"},"CreatePlaybookRequest":{"properties":{"segment":{"type":"string","title":"Segment"},"name":{"type":"string","title":"Name"},"role":{"type":"string","title":"Role"},"adp_band":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adp Band"}},"type":"object","required":["segment","name","role"],"title":"CreatePlaybookRequest"},"CreatePrincipalRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"display_name":{"type":"string","title":"Display Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"bound_to_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Bound To Principal Id"}},"type":"object","required":["kind","display_name"],"title":"CreatePrincipalRequest"},"CreateRoleBindingRequest":{"properties":{"function_key":{"type":"string","title":"Function Key"}},"type":"object","required":["function_key"],"title":"CreateRoleBindingRequest"},"CreateRoleRequest":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["name"],"title":"CreateRoleRequest"},"CreateSignalRequest":{"properties":{"signal_type":{"type":"string","title":"Signal Type"},"action_mapping":{"type":"string","title":"Action Mapping"},"org_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"},"payload":{"additionalProperties":true,"type":"object","title":"Payload","default":{}}},"type":"object","required":["signal_type","action_mapping"],"title":"CreateSignalRequest"},"CreateSupplySourceRequest":{"properties":{"source_key":{"type":"string","title":"Source Key"},"source_name":{"type":"string","title":"Source Name"},"lead_time_basis":{"type":"string","title":"Lead Time Basis"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"unit_price_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Unit Price Cents"},"price_basis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Basis"},"lead_time_days_low":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lead Time Days Low"},"lead_time_days_high":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lead Time Days High"},"is_preferred":{"type":"boolean","title":"Is Preferred","default":false}},"type":"object","required":["source_key","source_name","lead_time_basis"],"title":"CreateSupplySourceRequest"},"CreateTicketRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"source":{"type":"string","title":"Source"},"subject":{"type":"string","title":"Subject"},"org_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"},"requester_contact_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Requester Contact Id"},"requester_external_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester External Ref"},"requester_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester Email"},"assignee_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Assignee Principal Id"},"email_thread_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Thread Id"},"status":{"type":"string","title":"Status","default":"new"}},"type":"object","required":["kind","source","subject"],"title":"CreateTicketRequest"},"CreditInvoiceRequest":{"properties":{"amount":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Amount"},"reason":{"type":"string","minLength":1,"title":"Reason"},"credit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Credit Date"}},"type":"object","required":["amount","reason"],"title":"CreditInvoiceRequest"},"DaysOffBody":{"properties":{"first_day":{"type":"string","format":"date","title":"First Day"},"last_day":{"type":"string","format":"date","title":"Last Day"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["first_day","last_day"],"title":"DaysOffBody","description":"\"Away from ... to ...\", and why if they want to say."},"DealIn":{"properties":{"rate_per_adp":{"type":"string","maxLength":40,"title":"Rate Per Adp","default":""},"override_annual":{"type":"string","maxLength":40,"title":"Override Annual","default":""},"payment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment"},"pricing_mode":{"type":"string","maxLength":20,"title":"Pricing Mode","default":"per_adp"},"flat_annual":{"type":"string","maxLength":40,"title":"Flat Annual","default":""}},"type":"object","title":"DealIn","description":"The pricing panel (spec §1 \"Deal inputs\"). INTERNAL: stored on the\nrecord, never printed.\n\n`pricing_mode` (Frederick, 2026-09-30): \"per_adp\" (the spec's original\nrate x ADP x 12 calculation) or \"flat\" (a flat annual amount typed\ndirectly -- \"I can do 35k a year if I want flat rate\"). `rate_per_adp`/\n`override_annual` are read only in \"per_adp\" mode; `flat_annual` only in\n\"flat\" mode."},"DebitAnswerIn":{"properties":{"choice":{"type":"string","maxLength":40,"minLength":1,"title":"Choice"}},"type":"object","required":["choice"],"title":"DebitAnswerIn","description":"A tapped choice on the Today card (`payroll.answer_card`)."},"DecideProposal":{"properties":{"disposition":{"type":"string","title":"Disposition"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"}},"type":"object","required":["disposition"],"title":"DecideProposal"},"DecideSampleRequest":{"properties":{"decision":{"type":"string","title":"Decision"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["decision"],"title":"DecideSampleRequest"},"DecisionRequest":{"properties":{"latency_ms":{"type":"integer","maximum":86400000.0,"minimum":0.0,"title":"Latency Ms","default":0},"turns":{"anyOf":[{"type":"integer","maximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Turns"},"input_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Input Tokens"},"cache_creation_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Creation Tokens"},"cache_read_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Read Tokens"},"output_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Output Tokens"},"output":{"type":"string","maxLength":200000,"title":"Output"},"model":{"type":"string","maxLength":160,"minLength":1,"title":"Model"},"provider":{"type":"string","maxLength":64,"title":"Provider","default":"qwen_token_plan"}},"type":"object","required":["output","model"],"title":"DecisionRequest","description":"The lane's final message, UNPARSED (the server validates it), and what the run\ncost."},"DeclineBody":{"properties":{"reason":{"type":"string","maxLength":1000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"DeclineBody","description":"The supervisor's reason for declining a held credit. Required: a refusal says why."},"DeclineRepairBody":{"properties":{"note":{"type":"string","title":"Note"}},"type":"object","required":["note"],"title":"DeclineRepairBody"},"DeclineReprintRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"DeclineReprintRequest"},"DemoRequestBody":{"properties":{"facility_name":{"type":"string","maxLength":300,"minLength":1,"title":"Facility Name"},"org_type":{"anyOf":[{"type":"string","enum":["correctional_facility","partner","legal","advocacy","government_office"]},{"type":"null"}],"title":"Org Type"},"facility_type":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Facility Type"},"adp_estimate":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp Estimate"},"contact_name":{"type":"string","maxLength":200,"minLength":1,"title":"Contact Name"},"contact_title":{"type":"string","maxLength":200,"minLength":1,"title":"Contact Title"},"contact_email":{"type":"string","title":"Contact Email"},"contact_phone":{"type":"string","maxLength":40,"minLength":1,"title":"Contact Phone"},"guest_emails":{"items":{"type":"string"},"type":"array","title":"Guest Emails"},"tz":{"type":"string","title":"Tz"},"slot_start_utc":{"type":"string","format":"date-time","title":"Slot Start Utc"},"turnstile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token"},"company_website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Website"},"form_started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Form Started At"},"utm_source":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Source"},"utm_medium":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Medium"},"utm_campaign":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Campaign"},"utm_content":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Content"},"utm_term":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Term"},"landing_path":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Landing Path"},"referrer":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Referrer"}},"type":"object","required":["facility_name","contact_name","contact_title","contact_email","contact_phone","tz","slot_start_utc"],"title":"DemoRequestBody"},"DemoVisitBody":{"properties":{"path":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Path","default":"/demo"},"utm_source":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Source"},"utm_medium":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Medium"},"utm_campaign":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Campaign"},"utm_content":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Content"},"utm_term":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Utm Term"},"referrer":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Referrer"}},"type":"object","title":"DemoVisitBody","description":"What the page knows about this load: its own path and the attribution\n`captureAttribution()` already read for the booking. Cut to\n`ATTRIBUTION_MAX_LENGTH`, never refused, for the same reason as\n`DemoRequestBody._strip_attribution`."},"DemoteRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"demotion_reason":{"type":"string","title":"Demotion Reason"},"re_warm_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Re Warm At"},"re_warm_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Re Warm Reason"}},"type":"object","required":["org_id","demotion_reason"],"title":"DemoteRequest"},"DesignReviewRequest":{"properties":{"request_key":{"type":"string","maxLength":200,"minLength":1,"title":"Request Key"},"situation":{"type":"string","minLength":1,"title":"Situation"},"options":{"items":{"$ref":"#/components/schemas/OptionIn"},"type":"array","minItems":2,"title":"Options"},"recommendation":{"type":"string","minLength":1,"title":"Recommendation"}},"type":"object","required":["request_key","situation","options","recommendation"],"title":"DesignReviewRequest","description":"One design question for the board.\n\n`situation` is the whole evidence pack as the seats will read it (see\n`board_ask.ask_the_board_for_a_design` for why it is frozen here).\n`recommendation` is the filer's provisional pick; it is required by the\ndecision ledger and is NEVER shown to a seat (`board.blind_packet`).\nEvery option is priced the way the ledger prices any alternative\n(`board.routes.OptionIn`), including the required \"do nothing\"."},"DeskSessionUpsert":{"properties":{"session_file":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session File"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"},"goal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal Id"},"goal_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal Text"},"status":{"type":"string","title":"Status","default":"active"},"resumed":{"type":"boolean","title":"Resumed","default":false}},"type":"object","title":"DeskSessionUpsert"},"DeskUsageReport":{"properties":{"total_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Tokens"},"cost_usd_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cost Usd Cents"}},"type":"object","title":"DeskUsageReport"},"DevLoginRequest":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"DevLoginRequest"},"DialCheckResponse":{"properties":{"allowed":{"type":"boolean","title":"Allowed"},"rule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rule"},"refusal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Refusal"}},"type":"object","required":["allowed"],"title":"DialCheckResponse","description":"Whether `customer_dial.number_to_dial` allows this number for this\nticket. `refusal` is `CustomerDialRefused.as_wire()` when it does not."},"DialConfirmationRequest":{"properties":{"number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Number"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"DialConfirmationRequest"},"DialNumberRequest":{"properties":{"number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Number"}},"type":"object","title":"DialNumberRequest","description":"A number somebody is about to ring for a ticket. Empty means the\nnumber the ticket's own \"Call them back\" button would ring."},"DiscardDraftRequest":{"properties":{"fabricated":{"type":"boolean","title":"Fabricated","default":false}},"type":"object","title":"DiscardDraftRequest"},"DiscoverPeopleRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"title_query":{"type":"string","title":"Title Query"},"geography":{"type":"string","title":"Geography"},"limit":{"type":"integer","title":"Limit","default":25}},"type":"object","required":["org_id","title_query","geography"],"title":"DiscoverPeopleRequest"},"DismissRequest":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","title":"DismissRequest"},"DispositionResponse":{"properties":{"disposition_id":{"type":"string","title":"Disposition Id"},"ticket_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ticket Id"},"ticket_moved_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ticket Moved To"}},"type":"object","required":["disposition_id","ticket_id","ticket_moved_to"],"title":"DispositionResponse"},"DisputeIn":{"properties":{"note":{"type":"string","maxLength":500,"minLength":1,"title":"Note"}},"type":"object","required":["note"],"title":"DisputeIn"},"DisqualifyLeadRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"DisqualifyLeadRequest"},"DocumentRefIn":{"properties":{"storage":{"type":"string","title":"Storage"},"bucket":{"type":"string","title":"Bucket"},"object":{"type":"string","title":"Object"},"project":{"type":"string","title":"Project"},"generation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generation"},"content_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Type"},"size_bytes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size Bytes"},"md5":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Md5"},"crc32c":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crc32C"}},"type":"object","required":["storage","bucket","object","project"],"title":"DocumentRefIn","description":"Issue #2608: where to fetch this line's document BY IDENTITY,\nregardless of GCP project (Marc, 2026-09-30, verbatim: \"update the API\nso that clients send the required information to grab the file\nregardless of GCP project\").\n\n`storage`/`bucket`/`object` are required and load-bearing; `project` is\nrequired but is NOT what makes the fetch work -- see the design doc's\nown \"the project id is not what makes the fetch work\" section: bucket\nnames are globally unique in GCS and the grant is per-bucket, so one\nservice account with one grant per bucket reads across any number of\nprojects. `project` still earns its place for a 403's own IAM-page\npointer, a future requester-pays billing project, and a future\nper-project credential map (none of those three are wired by this\nbuild). `generation` is OPTIONAL but strongly wanted: pinned, a later\noverwrite of the same path cannot silently change what prints."},"DraftAttemptFailedRequest":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","title":"DraftAttemptFailedRequest","description":"⛔OPTIONAL, AND THE BODY ITSELF IS OPTIONAL TOO (see the route).\n\nThe same shape as `IntakeAttemptFailedRequest` below, deliberately: the\n`bug` surface has carried a failure reason since migration 0111 and\n`cs_draft` never did, so this is closing an asymmetry rather than\ninventing a vocabulary. `reason` is one of\n`tools.rooster_worker.worker`'s `FAILURE_*` constants."},"DraftFeedbackRequest":{"properties":{"feedback":{"type":"string","minLength":1,"title":"Feedback"}},"type":"object","required":["feedback"],"title":"DraftFeedbackRequest"},"DuplicateVerdictRequest":{"properties":{"verdict":{"type":"string","title":"Verdict"}},"type":"object","required":["verdict"],"title":"DuplicateVerdictRequest"},"EndEngagementBody":{"properties":{"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"EndEngagementBody"},"EnrichRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"person_ids":{"items":{"type":"string"},"type":"array","title":"Person Ids"}},"type":"object","required":["org_id","person_ids"],"title":"EnrichRequest"},"EnvelopeScanRequest":{"properties":{"code":{"type":"string","title":"Code"}},"type":"object","required":["code"],"title":"EnvelopeScanRequest"},"EscalateTicketRequest":{"properties":{"reason":{"type":"string","title":"Reason"},"needed":{"type":"string","title":"Needed"},"related_ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Related Ticket Id"},"recipient_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recipient Email"}},"type":"object","required":["reason","needed"],"title":"EscalateTicketRequest"},"ExecutiveTurnFailedRequest":{"properties":{"packet_digest":{"type":"string","maxLength":128,"minLength":1,"title":"Packet Digest"},"reason":{"type":"string","maxLength":1000,"minLength":1,"title":"Reason"},"retry":{"type":"boolean","title":"Retry","default":false}},"type":"object","required":["packet_digest","reason"],"title":"ExecutiveTurnFailedRequest"},"ExecutiveTurnResultRequest":{"properties":{"packet_digest":{"type":"string","maxLength":128,"minLength":1,"title":"Packet Digest"},"output":{"type":"string","maxLength":200000,"title":"Output"},"model":{"type":"string","maxLength":160,"minLength":1,"title":"Model"},"provider":{"type":"string","maxLength":64,"title":"Provider","default":"qwen_token_plan"},"latency_ms":{"type":"integer","maximum":86400000.0,"minimum":0.0,"title":"Latency Ms","default":0},"turns":{"anyOf":[{"type":"integer","maximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Turns"},"input_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Input Tokens"},"cache_creation_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Creation Tokens"},"cache_read_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Read Tokens"},"output_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Output Tokens"}},"type":"object","required":["packet_digest","output","model"],"title":"ExecutiveTurnResultRequest","description":"The seat's final message and what the run cost, from the CLI's own usage block.\nCounts are optional: a run that reported none files a row with NULL tokens, never\nzero."},"ExpectedRepairRequest":{"properties":{"expected_repair_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expected Repair At"}},"type":"object","required":["expected_repair_at"],"title":"ExpectedRepairRequest"},"ExplainDelayRequest":{"properties":{"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"ExplainDelayRequest"},"FailedBuildRequest":{"properties":{"reason":{"type":"string","title":"Reason"},"outcome":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outcome"}},"type":"object","required":["reason"],"title":"FailedBuildRequest"},"FailedRequest":{"properties":{"latency_ms":{"type":"integer","maximum":86400000.0,"minimum":0.0,"title":"Latency Ms","default":0},"turns":{"anyOf":[{"type":"integer","maximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Turns"},"input_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Input Tokens"},"cache_creation_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Creation Tokens"},"cache_read_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Read Tokens"},"output_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Output Tokens"},"reason":{"type":"string","maxLength":1000,"minLength":1,"title":"Reason"},"retry":{"type":"boolean","title":"Retry","default":false},"paused":{"type":"boolean","title":"Paused","default":false},"model":{"anyOf":[{"type":"string","maxLength":160,"minLength":1},{"type":"null"}],"title":"Model"},"provider":{"type":"string","maxLength":64,"title":"Provider","default":"qwen_token_plan"}},"type":"object","required":["reason"],"title":"FailedRequest"},"FileIn":{"properties":{"agreed_amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"},{"type":"null"}],"title":"Agreed Amount"}},"type":"object","title":"FileIn","description":"The body of Owe it. Empty for a bill to review; `agreed_amount` is\nrequired for a bill that was in dispute."},"FileReprintRequest":{"properties":{"parcel_id":{"type":"string","format":"uuid","title":"Parcel Id"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["parcel_id","reason"],"title":"FileReprintRequest"},"FileWorkOrderRequest":{"properties":{"title":{"type":"string","title":"Title"},"brief":{"type":"string","title":"Brief"},"ruling_reference":{"type":"string","title":"Ruling Reference"},"priority":{"type":"string","title":"Priority","default":"normal"},"base_branch":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Base Branch"},"target_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target System"}},"type":"object","required":["title","brief","ruling_reference"],"title":"FileWorkOrderRequest"},"FindSourcesRequest":{"properties":{"machine_model":{"type":"string","title":"Machine Model","default":"Pro C5300S"}},"type":"object","title":"FindSourcesRequest"},"FixIn":{"properties":{"payee":{"anyOf":[{"type":"string","maxLength":160,"minLength":1},{"type":"null"}],"title":"Payee"},"invoice_number":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Invoice Number"},"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"},{"type":"null"}],"title":"Amount"},"clear_amount":{"type":"boolean","title":"Clear Amount","default":false},"due_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Due Date"},"kind":{"anyOf":[{"type":"string","enum":["fixed_recurring","invoiced_variable","usage_reload"]},{"type":"null"}],"title":"Kind"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"bill_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Bill Id"},"clear_bill":{"type":"boolean","title":"Clear Bill","default":false}},"type":"object","title":"FixIn","description":"What Frederick corrects. Only the fields sent change."},"FlagAddressChangedRequest":{"properties":{"new_address":{"type":"string","title":"New Address"}},"type":"object","required":["new_address"],"title":"FlagAddressChangedRequest"},"FollowUpIn":{"properties":{"note":{"type":"string","maxLength":4000,"title":"Note"}},"type":"object","required":["note"],"title":"FollowUpIn"},"FramingRequest":{"properties":{"seat_key":{"type":"string","title":"Seat Key"},"framing_notes":{"type":"string","title":"Framing Notes"},"added_options":{"items":{"$ref":"#/components/schemas/OptionIn"},"type":"array","title":"Added Options"},"run":{"$ref":"#/components/schemas/RunReport"}},"type":"object","required":["seat_key","framing_notes","run"],"title":"FramingRequest","description":"One seat's blind read, before it has seen anyone else's."},"GenerateFromTermRequest":{"properties":{"term_id":{"type":"string","format":"uuid","title":"Term Id"},"invoice_date":{"type":"string","format":"date","title":"Invoice Date"},"piece_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Piece Count"}},"type":"object","required":["term_id","invoice_date"],"title":"GenerateFromTermRequest","description":"/invoices/new — the contract-driven creation screen. v1's POST\n/generate-from-contract mapped onto a term line: `term_id` is the line\nthe picker resolved as covering the chosen invoice_date; every dollar is\nderived from it server-side (service.generate_invoice_from_term)."},"GenerateIn":{"properties":{"kind":{"type":"string","title":"Kind"},"contact_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contact Id"},"answers":{"additionalProperties":{"type":"string"},"type":"object","title":"Answers"},"picked":{"additionalProperties":true,"type":"object","title":"Picked"},"deal":{"$ref":"#/components/schemas/DealIn"},"rep_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Rep Principal Id"},"regenerated_from_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Regenerated From Id"},"deal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Deal Id"}},"type":"object","required":["kind"],"title":"GenerateIn"},"GeneratorSettingsIn":{"properties":{"upfront_threshold":{"type":"number","exclusiveMinimum":0.0,"title":"Upfront Threshold"},"pigeonly_notice_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pigeonly Notice Name"},"support_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Support Email"},"original_retention_days":{"type":"integer","minimum":1.0,"title":"Original Retention Days"}},"type":"object","required":["upfront_threshold","original_retention_days"],"title":"GeneratorSettingsIn","description":"design/Org Documents.dc.html L326-336, \"Generator settings\": the four\nfields that card shows. `follow_up_schedule_days`/`stale_after_days`\n(spec §9/§10, Frederick 2026-10-01) are part of the same stored\n`PROPOSAL_GENERATOR` setting but have no field on this card -- the PUT\nroute below merges this body onto the setting's CURRENT value for\nthose two rather than asking this screen to carry fields it does not\nshow."},"GoalAmendment":{"properties":{"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"},"target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target"},"deadline":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Deadline"},"narrative":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Narrative"},"rationale":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rationale"},"goal_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Goal Kind"},"window_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Window Days"},"staleness_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staleness Days"},"horizon_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Horizon Days"},"priority_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority Rank"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"}},"type":"object","required":["reason"],"title":"GoalAmendment","description":"A change to a goal, and why.\n\n⛔EVERY FIELD IS OPTIONAL AND `None` IS AMBIGUOUS, so the route reads\n`model_dump(exclude_unset=True)` rather than the model's attributes:\n\"clear the deadline\" (`deadline: null`, sent) and \"do not touch the\ndeadline\" (not sent) are different instructions that both arrive as\n`None` on the object. Reading the attribute would make one of them\nimpossible, and it would be the clearing one -- a deadline nobody could\never remove."},"GoalConfirmation":{"properties":{"kind":{"type":"string","maxLength":40,"minLength":1,"title":"Kind"},"quote":{"type":"string","maxLength":4000,"minLength":1,"title":"Quote"},"reading":{"type":"string","maxLength":2000,"minLength":1,"title":"Reading"},"consequence":{"type":"string","maxLength":2000,"minLength":1,"title":"Consequence"}},"type":"object","required":["kind","quote","reading","consequence"],"title":"GoalConfirmation"},"GoalIn":{"properties":{"metric_key":{"type":"string","maxLength":120,"minLength":1,"title":"Metric Key"},"target":{"type":"string","title":"Target"},"deadline":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Deadline"},"narrative":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Narrative"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"}},"type":"object","required":["metric_key","target"],"title":"GoalIn"},"GoalPurge":{"properties":{"note":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Note"}},"type":"object","title":"GoalPurge"},"GoalRetirement":{"properties":{"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"GoalRetirement"},"GoalTranscript":{"properties":{"transcript":{"type":"string","maxLength":20000,"minLength":1,"title":"Transcript"}},"type":"object","required":["transcript"],"title":"GoalTranscript"},"GrantBody":{"properties":{"user_id":{"type":"string","format":"uuid","title":"User Id"},"level":{"type":"string","title":"Level"}},"type":"object","required":["user_id","level"],"title":"GrantBody"},"GuestBody":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"email":{"type":"string","maxLength":320,"title":"Email"},"title":{"type":"string","maxLength":200,"title":"Title","default":""}},"type":"object","required":["name","email"],"title":"GuestBody","description":"Name AND email both required. An address that appeared once is not a\nperson on the organisation, which is why this is not a bare email list."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HandMeetingRequest":{"properties":{"host_principal_id":{"type":"string","format":"uuid","title":"Host Principal Id"},"due_at":{"type":"string","format":"date-time","title":"Due At"},"subject":{"type":"string","maxLength":500,"minLength":1,"title":"Subject"}},"type":"object","required":["host_principal_id","due_at","subject"],"title":"HandMeetingRequest"},"HandoffRequest":{"properties":{"to_principal_id":{"type":"string","title":"To Principal Id"},"note":{"type":"string","title":"Note","default":""}},"type":"object","required":["to_principal_id"],"title":"HandoffRequest"},"HandoffResult":{"properties":{"said":{"type":"string","title":"Said"}},"type":"object","required":["said"],"title":"HandoffResult"},"HarnessFailedBuildRequest":{"properties":{"failure_class":{"type":"string","title":"Failure Class"},"detail":{"type":"string","title":"Detail","default":""}},"type":"object","required":["failure_class"],"title":"HarnessFailedBuildRequest"},"HeartbeatBody":{"properties":{"product_type":{"type":"string","title":"Product Type"},"queue_depth":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Queue Depth"},"down":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Down"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"agent_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version"},"devices":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Devices"}},"type":"object","required":["product_type"],"title":"HeartbeatBody"},"HireBody":{"properties":{"full_name":{"type":"string","maxLength":200,"minLength":1,"title":"Full Name"},"contact":{"type":"string","maxLength":320,"minLength":1,"title":"Contact"},"phone":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Phone"},"start_date":{"type":"string","format":"date","title":"Start Date"},"department":{"type":"string","title":"Department","default":"fulfillment"},"principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Principal Id"}},"type":"object","required":["full_name","contact","start_date"],"title":"HireBody"},"HourlyRateRequest":{"properties":{"dollars_per_hour":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dollars Per Hour"}},"type":"object","required":["dollars_per_hour"],"title":"HourlyRateRequest"},"IdentifySenderRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"}},"type":"object","required":["org_id"],"title":"IdentifySenderRequest"},"ImportTicketValues":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object","title":"Values"}},"type":"object","required":["values"],"title":"ImportTicketValues","description":"The values a launcher writes with an approved ticket: a map from vault\nname to secret. The ticket's scope decides which names are accepted."},"IncomingAnswerIn":{"properties":{"choice":{"type":"string","maxLength":40,"minLength":1,"title":"Choice"}},"type":"object","required":["choice"],"title":"IncomingAnswerIn","description":"A tapped choice on the Today card (`incoming.answer_card`)."},"IngestConfirmationEmailRequest":{"properties":{"raw_text":{"type":"string","title":"Raw Text"},"sender":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender"}},"type":"object","required":["raw_text"],"title":"IngestConfirmationEmailRequest"},"IngestRequest":{"properties":{"mailbox":{"type":"string","title":"Mailbox"}},"type":"object","required":["mailbox"],"title":"IngestRequest"},"IngestSignalRequest":{"properties":{"payload":{"additionalProperties":true,"type":"object","title":"Payload"}},"type":"object","required":["payload"],"title":"IngestSignalRequest"},"IntakeAttemptFailedRequest":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","title":"IntakeAttemptFailedRequest"},"InvoiceIn":{"properties":{"invoice_number":{"type":"string","maxLength":80,"minLength":1,"title":"Invoice Number"},"invoice_date":{"type":"string","format":"date","title":"Invoice Date"},"due_date":{"type":"string","format":"date","title":"Due Date"},"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Amount"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"}},"type":"object","required":["invoice_number","invoice_date","due_date","amount"],"title":"InvoiceIn"},"IssueManualInvoiceRequest":{"properties":{"term_id":{"type":"string","format":"uuid","title":"Term Id"},"invoice_date":{"type":"string","format":"date","title":"Invoice Date"},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"due_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Due Date"},"billing_period_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Billing Period Start"},"billing_period_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Billing Period End"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"line_items":{"items":{"$ref":"#/components/schemas/ManualLineItemBody"},"type":"array","title":"Line Items"}},"type":"object","required":["term_id","invoice_date","line_items"],"title":"IssueManualInvoiceRequest","description":"/invoices/issue-manual — v1's POST /invoices/issue-manual mapped onto\na term line. Quantities/period/due-date/terms pre-fill from GET\n/invoices/manual-prefill; the caller types the dollar figures, validated\nserver-side by validation.validate_manual_invoice (ported verbatim from\nv1's manual-invoice.ts — a blank/negative/absent rate or total is\nrejected here, not silently accepted the way the pre-existing bare\nCreateInvoiceModal / POST /invoices path is)."},"IssueTurnRequest":{"properties":{"reply":{"type":"string","title":"Reply"},"trace_steps":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Trace Steps"},"draft":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Draft"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"},"claude_session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Claude Session Id"}},"type":"object","required":["reply"],"title":"IssueTurnRequest"},"KeepCommitmentRequest":{"properties":{"kept_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Kept At"},"kept_evidence":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kept Evidence"}},"type":"object","title":"KeepCommitmentRequest"},"KeepMyShiftBody":{"properties":{"keep":{"type":"boolean","title":"Keep"}},"type":"object","required":["keep"],"title":"KeepMyShiftBody"},"LateNoticeRequest":{"properties":{"subject_kind":{"type":"string","title":"Subject Kind"},"subject_id":{"type":"string","format":"uuid","title":"Subject Id"},"initiated_by":{"type":"string","title":"Initiated By"},"note":{"type":"string","title":"Note"}},"type":"object","required":["subject_kind","subject_id","initiated_by","note"],"title":"LateNoticeRequest","description":"`{ subject_kind, subject_id, initiated_by, note }` -- him flagging,\nafter the fact, that something should have reached him sooner."},"LearnedAuthorityRequest":{"properties":{"rung":{"type":"string","title":"Rung"},"evidence":{"type":"string","title":"Evidence"}},"type":"object","required":["rung","evidence"],"title":"LearnedAuthorityRequest"},"LifecycleIn":{"properties":{"action":{"type":"string","enum":["pause","resume","end"],"title":"Action"}},"type":"object","required":["action"],"title":"LifecycleIn"},"LineItem":{"properties":{"line_item_id":{"type":"string","title":"Line Item Id"},"product_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product Id"},"quantity":{"type":"integer","title":"Quantity","default":1},"document_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Url"},"document":{"anyOf":[{"$ref":"#/components/schemas/DocumentRefIn"},{"type":"null"}]},"parcel_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Code"}},"type":"object","required":["line_item_id"],"title":"LineItem"},"LineItemBody":{"properties":{"description":{"type":"string","title":"Description"},"quantity":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Quantity","default":"1"},"rate":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Rate","default":"0"},"amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Amount"}},"type":"object","required":["description","amount"],"title":"LineItemBody"},"LoseDealRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"LoseDealRequest"},"ManualLineItemBody":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"quantity":{"title":"Quantity"},"unit_price":{"title":"Unit Price"},"total":{"title":"Total"}},"type":"object","title":"ManualLineItemBody","description":"One line of /invoices/issue-manual's payload. Dollar fields are typed\nloosely (`Any`, matching v1's `string | number | null` wire) rather than\n`Decimal | None` so a blank string or NaN reaches\nvalidation.validate_manual_invoice for its own v1-worded rejection\nmessage, instead of pydantic's own generic coercion error."},"ManualSendInvoiceRequest":{"properties":{"invoice_id":{"type":"string","format":"uuid","title":"Invoice Id"},"to_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To Email"}},"type":"object","required":["invoice_id"],"title":"ManualSendInvoiceRequest"},"MarkBatchFailedRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"MarkBatchFailedRequest"},"MarkNotATicketRequest":{"properties":{"scope":{"type":"string","title":"Scope","default":"address"}},"type":"object","title":"MarkNotATicketRequest","description":"Frederick, 2026-10-06 (G-61201): \"its not spam but its also not real CS\ntickets\". `scope` is 'address' by default and 'domain' for \"everything\nfrom this domain\" -- the same two scopes, same default and same\nfree-mail refusal as `MarkSpamRequest`."},"MarkSpamRequest":{"properties":{"scope":{"type":"string","title":"Scope","default":"address"},"resolve_action_item_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Resolve Action Item Id"}},"type":"object","title":"MarkSpamRequest","description":"Frederick, 2026-07-30: \"I should just block that sender, and then\nclose that ticket out and remove it from the queue.\" `scope` defaults to\n'address' -- deliberately narrower than not_interested's domain default;\na domain-wide block must be a choice the caller makes, never automatic."},"MatchOrganizationRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"}},"type":"object","required":["org_id"],"title":"MatchOrganizationRequest"},"MergedFixRequest":{"properties":{"pr_url":{"type":"string","title":"Pr Url"},"branch":{"type":"string","title":"Branch"},"summary":{"type":"string","title":"Summary"},"how_to_check":{"type":"string","title":"How To Check","default":""},"outcome":{"type":"string","title":"Outcome","default":""},"still_off":{"type":"string","title":"Still Off","default":""},"upstream_answer":{"type":"string","title":"Upstream Answer","default":""}},"type":"object","required":["pr_url","branch","summary"],"title":"MergedFixRequest","description":"The same record, for a merge rather than a staging proof.\n\nSplit off from StagedFixRequest for one field, because the two writes\ndiffer in kind. `/staged` ANNOUNCES something to a person and must not\nhappen without an instruction for them. `/merged` REMEMBERS something\nthat has already happened and cannot be undone -- refusing it does not\nun-merge the commit, it only loses the fact that main now carries it."},"MeterOverrideRequest":{"properties":{"interval":{"type":"integer","title":"Interval"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["interval","reason"],"title":"MeterOverrideRequest"},"MintUnsubscribeTokenRequest":{"properties":{"identifier_type":{"type":"string","title":"Identifier Type"},"identifier":{"type":"string","title":"Identifier"},"org_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Org Id"},"send_log_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Send Log Id"}},"type":"object","required":["identifier_type","identifier"],"title":"MintUnsubscribeTokenRequest"},"MoneyActBody":{"properties":{"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"},"call_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Call Id"},"request_id":{"type":"string","format":"uuid","title":"Request Id"},"suggestion_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Suggestion Id"},"contact_reason":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Contact Reason"},"kind":{"type":"string","enum":["refund","free_credit","free_months","legacy_refund"],"title":"Kind"},"reason":{"type":"string","maxLength":1000,"minLength":1,"title":"Reason"},"amount_cents":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Amount Cents"},"invoice_number":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Invoice Number"},"months":{"anyOf":[{"type":"integer","maximum":12.0,"minimum":1.0},{"type":"null"}],"title":"Months"},"subscription_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Subscription Id"},"legacy_order_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Legacy Order Id"},"call_reason_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Call Reason Id"}},"type":"object","required":["request_id","kind","reason"],"title":"MoneyActBody","description":"A money act's request. `request_id` is REQUIRED (second review of PR #4160, finding 9):\nit is the act ledger's idempotency key, so a refund, a credit or free months asked twice\nby one screen is ONE act only when both asks carry the same id. A body without one used\nto be given a fresh id by `acts.perform` and so had no protection against a double tap\nor a retry. (`resend` needs no such rule: its provider key is the send itself.)"},"MoveBatchRequest":{"properties":{"printer":{"type":"string","title":"Printer"},"direction":{"type":"string","title":"Direction"}},"type":"object","required":["printer","direction"],"title":"MoveBatchRequest"},"MoveBlockBody":{"properties":{"date":{"type":"string","format":"date","title":"Date"},"start":{"type":"string","title":"Start"},"end":{"type":"string","title":"End"}},"type":"object","required":["date","start","end"],"title":"MoveBlockBody","description":"The new time for a block a person holds, in FLOOR-LOCAL time\n(America/Los_Angeles): a calendar day and two clock times. An `end` at or\nbefore the `start` means the next calendar day (the same rule\n`domain.crosses_midnight` uses for the overnight window), so the browser\nnever builds a UTC instant or guesses at daylight saving."},"MoveDealRequest":{"properties":{"to_stage":{"type":"string","title":"To Stage"}},"type":"object","required":["to_stage"],"title":"MoveDealRequest"},"MoveLeadRequest":{"properties":{"to_stage":{"type":"string","title":"To Stage"}},"type":"object","required":["to_stage"],"title":"MoveLeadRequest"},"NewVendorIn":{"properties":{"company_name":{"type":"string","maxLength":200,"minLength":1,"title":"Company Name"}},"type":"object","required":["company_name"],"title":"NewVendorIn"},"NotInterestedRequest":{"properties":{"scope":{"type":"string","title":"Scope","default":"domain"},"reason":{"type":"string","title":"Reason","default":"not_interested"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"this_queue_only":{"type":"boolean","title":"This Queue Only","default":false}},"type":"object","title":"NotInterestedRequest","description":"Flagging mail we never want again.\n\n`scope` defaults to the whole sending domain, because that is what\nactually ends the problem: marketers rotate research@ to insights@ to\nnewsletter@ within a week, and suppressing one address at a time is the\n\"doing it more than once\" this feature exists to stop. Free-mail domains\nare refused for domain scope -- see suppression.resolve_target."},"OffboardCustomerRequest":{"properties":{"final_scan_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Final Scan Date"},"cancellation_confirmed_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Cancellation Confirmed Date"},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"}},"type":"object","title":"OffboardCustomerRequest","description":"Recording that a customer left.\n\nBOTH DATES OPTIONAL, and that is deliberate rather than lax. A record with\nneither means an immediate stop (billing.last_billable_day), so somebody\nwho knows only that a customer cancelled can say so without inventing a\ndate. Frederick, 2026-08-21, establishing three real cancellation dates:\ntwo of them took several exchanges and one was still only inferable from\nthe last invoice. A guessed date presented as a known one is how a refund\ngets computed wrong."},"OpenBoxRequest":{"properties":{"facility_id":{"type":"string","format":"uuid","title":"Facility Id"}},"type":"object","required":["facility_id"],"title":"OpenBoxRequest"},"OpeningBalanceIn":{"properties":{"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Amount"},"as_of":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of"}},"type":"object","required":["amount"],"title":"OpeningBalanceIn"},"OperatingCashIn":{"properties":{"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Amount"},"as_of":{"type":"string","format":"date","title":"As Of"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"}},"type":"object","required":["amount","as_of"],"title":"OperatingCashIn"},"OptionIn":{"properties":{"label":{"type":"string","title":"Label"},"revenue_argument":{"type":"string","title":"Revenue Argument"},"confidence":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Confidence"},"cost_delta_usd":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Delta Usd"},"cost_unknown_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cost Unknown Reason"},"horizon_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Horizon Days"},"horizon_unknown_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Horizon Unknown Reason"}},"type":"object","required":["label","revenue_argument","confidence"],"title":"OptionIn","description":"An option a seat is adding to the ballot -- the SAME shape the decision\nledger prices its own alternatives in.\n\n⛔`revenue_argument` AND `confidence` ARE REQUIRED, and the unpriced fields\ncarry their reason in words. A seat's own added option is priced exactly\nthe way the CEO's are (`board/domain.py`'s own top comment): an option\nnobody argued for is not an alternative, and a `None` cost with no\n`cost_unknown_reason` is the blank-is-a-lie rule in its purest form --\n`Option.__post_init__` refuses both."},"OriginationFailedRequest":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"to_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To Number"},"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"}},"type":"object","title":"OriginationFailedRequest","description":"What the agent's browser says went wrong.\n\n⛔EVERY FIELD IS SELF-REPORTED AND UNTRUSTED. The softphone dials\nbrowser-to-Plivo directly, so this is the only account of a refusal that\nexists -- Plivo may hold nothing for an origination it refused. Bounded\nand stored opaque; never interpolated, never rendered as markup."},"OutboundChannelResponse":{"properties":{"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"cell_phone_last4":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cell Phone Last4"}},"type":"object","required":["channel","reason","cell_phone_last4"],"title":"OutboundChannelResponse","description":"Which of the two outbound paths the keypad should use, decided before\nit ever asks for a browser token or dials anything.\n\n⛔WHY THE WEB ASKS THIS FIRST, 2026-09-23. Frederick: \"for US people the\ndial out should connect through their cell phone; for people outside the\nUS the call needs to go through the softphone.\" A US principal is never\nminted a softphone endpoint at all now (`browser_token` refuses one), so\nthe keypad cannot simply try `/browser-token` and fall back on failure --\nthat would provision nothing, mint nothing, and read as the exact silent\n\"nothing happened\" Frederick has reported twice already on this same\npanel. This route answers the question directly, from the already\n-resolved `Principal.work_country`/`cell_phone` (no extra query), so the\nkeypad knows which of the two flows to run before it does anything."},"OutsideCallingHoursBody":{"properties":{"detail":{"type":"string","title":"Detail"},"next_allowed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Allowed At"},"customer_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Zone"}},"type":"object","required":["detail"],"title":"OutsideCallingHoursBody","description":"The 409 a ticket dial answers when it is outside the customer's\ncalling hours (Frederick, 2026-09-24): the sentence, plus the next moment\nthe call is allowed and the customer's zone, exactly as\n`callback_licence.may_we_dial` decided them. See\n`ringing_back.OutsideCallingHours`."},"OverrideRequest":{"properties":{"target_table":{"type":"string","title":"Target Table"},"target_id":{"type":"string","format":"uuid","title":"Target Id"},"field":{"type":"string","title":"Field"},"new_value":{"title":"New Value"},"reason":{"type":"string","minLength":1,"title":"Reason"}},"type":"object","required":["target_table","target_id","field","reason"],"title":"OverrideRequest"},"PackagingScanRequest":{"properties":{"raw":{"type":"string","title":"Raw"}},"type":"object","required":["raw"],"title":"PackagingScanRequest"},"PartReplacedRequest":{"properties":{"part_name":{"type":"string","title":"Part Name"},"part_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Part Number"},"page_count_at_replacement":{"type":"integer","title":"Page Count At Replacement"},"cost_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cost Cents"},"who":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Who"}},"type":"object","required":["part_name","page_count_at_replacement"],"title":"PartReplacedRequest","description":"The simple form on the Equipment page (docs/01_decisions/rulings/\n2026-09-30-rooster-is-the-ricoh-technician.md) -- the second of the two\nentry points that write a `part_replaced` event, alongside a parsed\nservice document (`service.record_service_document`)."},"PayRunIn":{"properties":{"paid_on":{"type":"string","format":"date","title":"Paid On"},"total":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Total"},"items":{"items":{"$ref":"#/components/schemas/PayRunItemIn"},"type":"array","maxItems":200,"minItems":1,"title":"Items"},"reference":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Reference"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"},"transaction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transaction Id"}},"type":"object","required":["paid_on","total","items"],"title":"PayRunIn"},"PayRunItemIn":{"properties":{"bill_id":{"type":"string","format":"uuid","title":"Bill Id"},"period_due_on":{"type":"string","format":"date","title":"Period Due On"},"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Amount"}},"type":"object","required":["bill_id","period_due_on","amount"],"title":"PayRunItemIn","description":"One person's period in a pay run: a bill id (the person's salary line),\nthe due date of the period, and how much of it this run paid (a part of a\nperiod is allowed)."},"PaymentIn":{"properties":{"transaction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transaction Id"},"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"},{"type":"null"}],"title":"Amount"},"paid_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Paid On"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"},"invoice_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Invoice Id"},"period_due_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Period Due On"}},"type":"object","title":"PaymentIn","description":"Count a payment against this bill. Either name a bank transaction\n(\"this payment belongs to this bill\") or record one made another way."},"PaymentRowIn":{"properties":{"paid_on":{"type":"string","format":"date","title":"Paid On"},"line_id":{"type":"string","maxLength":200,"minLength":1,"title":"Line Id"},"amount":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"integer"}],"title":"Amount"},"reference":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Reference"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Note"},"split_group":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Split Group"}},"type":"object","required":["paid_on","line_id","amount"],"title":"PaymentRowIn","description":"One row of the entry list (the frontend's `DebtPaymentBody`)."},"PaymentsIn":{"properties":{"payments":{"items":{"$ref":"#/components/schemas/PaymentRowIn"},"type":"array","maxItems":200,"minItems":1,"title":"Payments"},"confirm_duplicate":{"type":"boolean","title":"Confirm Duplicate","default":false}},"type":"object","required":["payments"],"title":"PaymentsIn"},"PhoneHealthOut":{"properties":{"day":{"type":"string","title":"Day"},"calls":{"type":"integer","title":"Calls"},"rooster_spoke":{"type":"integer","title":"Rooster Spoke"},"reached_a_person":{"type":"integer","title":"Reached A Person"},"callbacks_booked":{"type":"integer","title":"Callbacks Booked"},"callbacks_completed":{"type":"integer","title":"Callbacks Completed"},"callbacks_waiting":{"type":"integer","title":"Callbacks Waiting"},"carrier_calls":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Carrier Calls"},"missed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Missed"},"answering":{"type":"boolean","title":"Answering"},"answering_detail":{"type":"string","title":"Answering Detail"},"answer_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Url"},"answer_url_is_ours":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Answer Url Is Ours"},"browser_answer_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser Answer Url"},"browser_answer_url_is_ours":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Browser Answer Url Is Ours"},"browser_hangup_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser Hangup Url"},"browser_hangup_url_is_ours":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Browser Hangup Url Is Ours"}},"type":"object","required":["day","calls","rooster_spoke","reached_a_person","callbacks_booked","callbacks_completed","callbacks_waiting","carrier_calls","missed","answering","answering_detail","answer_url","answer_url_is_ours","browser_answer_url","browser_answer_url_is_ours","browser_hangup_url","browser_hangup_url_is_ours"],"title":"PhoneHealthOut","description":"Today's phone, and whether it is up."},"PhoneScoreOut":{"properties":{"days":{"type":"integer","title":"Days"},"calls":{"type":"integer","title":"Calls"},"reached_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reached Pct"},"abandoned_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Abandoned Pct"},"after_hours_calls":{"type":"integer","title":"After Hours Calls"},"after_hours_reached_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"After Hours Reached Pct"},"baseline_reached_pct":{"type":"number","title":"Baseline Reached Pct"},"baseline_abandoned_pct":{"type":"number","title":"Baseline Abandoned Pct"},"baseline_after_hours_reached_pct":{"type":"number","title":"Baseline After Hours Reached Pct"},"baseline_calls":{"type":"integer","title":"Baseline Calls"},"beats_baseline":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Beats Baseline"}},"type":"object","required":["days","calls","reached_pct","abandoned_pct","after_hours_calls","after_hours_reached_pct","baseline_reached_pct","baseline_abandoned_pct","baseline_after_hours_reached_pct","baseline_calls","beats_baseline"],"title":"PhoneScoreOut"},"PickBody":{"properties":{"block_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Block Id"},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"},"rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rank"}},"type":"object","title":"PickBody","description":"Ask for a time window (`starts_at` and `ends_at`, as the picker\nreceived them) or, for an older client, one block by id. Exactly one of\nthe two."},"PickRequest":{"properties":{"option_id":{"type":"string","title":"Option Id"}},"type":"object","required":["option_id"],"title":"PickRequest","description":"`{ option_id: string }` -- `decisions.ts`'s `pickOption` body."},"PipelineFindingRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"subject":{"type":"string","title":"Subject"},"detail":{"type":"string","title":"Detail"},"reference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference"},"urgent":{"type":"boolean","title":"Urgent","default":false},"recipient_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recipient Email"}},"type":"object","required":["kind","subject","detail"],"title":"PipelineFindingRequest","description":"One row the deployer's watchdog observed this tick -- same shape\n`rooster/supervision_routes.py`'s own `ReportFindingRequest` uses,\nbecause this route's whole job is handing the same four rows\n(`supervision.KINDS`) into the SAME `record_finding` card/DM path, not\ninventing a second one for the release pipeline."},"PipelineFindingResolutionRequest":{"properties":{"reference":{"type":"string","title":"Reference"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["reference","detail"],"title":"PipelineFindingResolutionRequest","description":"The clearance for a condition the deployer reported under\n`reference` and has now OBSERVED healthy -- `supervision.\nrecord_resolution`'s own contract (\"only an actual successful probe\nmay call this\"): the deployer calls it from the tick that measured\nproduction current again, never from silence."},"PlaybookTurnRequest":{"properties":{"reply":{"type":"string","title":"Reply"},"trace_steps":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Trace Steps"},"draft":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Draft"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"}},"type":"object","required":["reply"],"title":"PlaybookTurnRequest"},"PositionIn":{"properties":{"option_label":{"type":"string","title":"Option Label"},"stance":{"type":"string","title":"Stance"},"rationale":{"type":"string","title":"Rationale"}},"type":"object","required":["option_label","stance","rationale"],"title":"PositionIn"},"PositionsRequest":{"properties":{"seat_key":{"type":"string","title":"Seat Key"},"positions":{"items":{"$ref":"#/components/schemas/PositionIn"},"type":"array","title":"Positions"},"run":{"$ref":"#/components/schemas/RunReport"}},"type":"object","required":["seat_key","positions","run"],"title":"PositionsRequest","description":"One seat's full vote: a stance on every ballot item."},"PressTheSwitchRequest":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","title":"PressTheSwitchRequest","description":"The optional reason a person gives when they press Start or Pause printing."},"PrintResultBody":{"properties":{"printed":{"type":"boolean","title":"Printed"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["printed"],"title":"PrintResultBody"},"PromoteOrgToDealRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"path":{"type":"string","title":"Path"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"},"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"amount_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Cents"},"close_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Close Date"},"deal_size":{"type":"string","title":"Deal Size","default":"full"},"originated_by_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Originated By Principal Id"},"booked_it_myself":{"type":"boolean","title":"Booked It Myself","default":false}},"type":"object","required":["org_id","path"],"title":"PromoteOrgToDealRequest","description":"Organization Actions menu (build task \"flexible Actions menu\",\n2026-08-09): prospect -> deal, skipping the lead stage entirely --\nFrederick: \"it just skips that altogether, it just never was a lead.\"\nNo `lead_id` field -- unlike CreateDealRequest, this path never takes\none; promote_org_to_deal's own INSERT hardcodes it NULL."},"PromotionHoldBody":{"properties":{"kind":{"type":"string","title":"Kind"},"running_sha":{"type":"string","title":"Running Sha"},"held_at_sha":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Held At Sha"},"held_at_subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Held At Subject"},"blocked_by":{"items":{"type":"integer"},"type":"array","title":"Blocked By"},"waiting_pull_requests":{"items":{"type":"integer"},"type":"array","title":"Waiting Pull Requests"}},"type":"object","required":["kind","running_sha"],"title":"PromotionHoldBody","description":"One pass's decision, as `drain.promote_pass` made it. The shape is\nchecked by `settings_service._validate` (PROMOTION_HOLD), not duplicated\nhere: an unknown kind or a hold missing the fact its kind is about is a\n422 from that one place."},"PromotionHoldRequest":{"properties":{"hold":{"anyOf":[{"$ref":"#/components/schemas/PromotionHoldBody"},{"type":"null"}]}},"type":"object","title":"PromotionHoldRequest"},"PromptRecordingOut":{"properties":{"key":{"type":"string","title":"Key"},"text":{"type":"string","title":"Text"},"spanish":{"type":"boolean","title":"Spanish"},"source":{"type":"string","title":"Source"},"recorded_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recorded By"},"recorded_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Recorded At"},"audio_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Path"},"breaks_one_voice":{"type":"boolean","title":"Breaks One Voice","default":false},"mismatch_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mismatch Note"}},"type":"object","required":["key","text","spanish","source"],"title":"PromptRecordingOut"},"ProposePartOrderRequest":{"properties":{"part_number":{"type":"string","title":"Part Number"},"part_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Part Name"},"supplier_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Supplier Id"},"listing_url":{"type":"string","title":"Listing Url"},"price_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Cents"},"shipping_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Shipping Cents"},"condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condition"},"reason":{"type":"string","title":"Reason"},"quantity":{"type":"integer","title":"Quantity","default":1},"supplier_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Supplier Name"}},"type":"object","required":["part_number","listing_url","reason"],"title":"ProposePartOrderRequest"},"PublishVersionRequest":{"properties":{"strategy":{"type":"string","title":"Strategy"},"reasoning":{"type":"string","title":"Reasoning"},"cadence":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Cadence","default":[]},"plays":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Plays","default":[]},"copy":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Copy","default":[]},"collateral":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Collateral","default":[]}},"type":"object","required":["strategy","reasoning"],"title":"PublishVersionRequest"},"ReasonRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"ReasonRequest"},"ReassignRequest":{"properties":{"to_principal_id":{"type":"string","format":"uuid","title":"To Principal Id"}},"type":"object","required":["to_principal_id"],"title":"ReassignRequest"},"RecordArrivedRequest":{"properties":{"tracking_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tracking Number"}},"type":"object","title":"RecordArrivedRequest"},"RecordBackgroundCheckResultBody":{"properties":{"result":{"type":"string","title":"Result"},"vendor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vendor"},"vendor_reference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vendor Reference"}},"type":"object","required":["result"],"title":"RecordBackgroundCheckResultBody"},"RecordDeployEventRequest":{"properties":{"sha":{"type":"string","minLength":7,"title":"Sha"},"environment":{"type":"string","title":"Environment"},"started_at":{"type":"string","format":"date-time","title":"Started At"},"finished_at":{"type":"string","format":"date-time","title":"Finished At"},"ok":{"type":"boolean","title":"Ok"},"rolled_back":{"type":"boolean","title":"Rolled Back","default":false},"promoted_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Promoted By"}},"type":"object","required":["sha","environment","started_at","finished_at","ok"],"title":"RecordDeployEventRequest","description":"One deploy attempt, already finished by the time this is called --\nsee deploy_events_service.record_deploy_event's own docstring for why\nthis is an INSERT with both timestamps already known, never an\nupdate-in-place."},"RecordFeedbackRequest":{"properties":{"skill_key":{"type":"string","title":"Skill Key"},"draft_ref":{"type":"string","title":"Draft Ref"},"disposition":{"type":"string","title":"Disposition"},"edited_diff":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Edited Diff"},"vertical":{"type":"string","title":"Vertical","default":"b2c"},"outcome_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outcome Note"}},"type":"object","required":["skill_key","draft_ref","disposition"],"title":"RecordFeedbackRequest"},"RecordInstalledRequest":{"properties":{"page_count_at_replacement":{"type":"integer","title":"Page Count At Replacement"},"who":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Who"}},"type":"object","required":["page_count_at_replacement"],"title":"RecordInstalledRequest"},"RecordOrderedRequest":{"properties":{"ordered_by_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ordered By Text"},"supplier_order_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Supplier Order Number"},"expected_arrival_at":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Expected Arrival At"}},"type":"object","title":"RecordOrderedRequest"},"RecordOutcomeRequest":{"properties":{"resolution":{"type":"string","title":"Resolution"},"correction_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Correction Text"}},"type":"object","required":["resolution"],"title":"RecordOutcomeRequest","description":"`Rooster.dc.html`'s \"Close it\" card: \"Mark resolved\" is a person\naccepting Rooster's finding (`resolution=\"accepted\"`); no other\nresolution has a button on that screen today, but the full vocabulary\nis accepted here rather than hardcoding one string, since this is the\nsame `record_outcome` seam every other case-closing caller uses."},"RecordPaymentRequest":{"properties":{"method":{"type":"string","title":"Method"},"amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Amount"},"payment_date":{"type":"string","format":"date","title":"Payment Date"},"reference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference"}},"type":"object","required":["method","amount","payment_date"],"title":"RecordPaymentRequest"},"RecordPayoutRequest":{"properties":{"recipient_principal_id":{"type":"string","format":"uuid","title":"Recipient Principal Id"},"kind":{"type":"string","title":"Kind"},"amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Amount"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"obligation_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Obligation Ids","default":[]},"advance":{"type":"boolean","title":"Advance","default":true}},"type":"object","required":["recipient_principal_id","kind","amount"],"title":"RecordPayoutRequest"},"RecordPostSendSignalRequest":{"properties":{"skill_key":{"type":"string","title":"Skill Key"},"draft_ref":{"type":"string","title":"Draft Ref"},"signal_type":{"type":"string","title":"Signal Type"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["skill_key","draft_ref","signal_type"],"title":"RecordPostSendSignalRequest"},"RecordPrReviewOverrideRequest":{"properties":{"pr_number":{"type":"integer","title":"Pr Number"},"head_sha":{"type":"string","title":"Head Sha"},"actor":{"type":"string","title":"Actor"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["pr_number","head_sha","actor"],"title":"RecordPrReviewOverrideRequest"},"RecordPrReviewVerdictRequest":{"properties":{"verdict":{"type":"string","title":"Verdict"},"findings":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Findings","default":[]},"comment_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Comment Url"}},"type":"object","required":["verdict"],"title":"RecordPrReviewVerdictRequest"},"RecordReferralRequest":{"properties":{"from_contact_id":{"type":"string","format":"uuid","title":"From Contact Id"},"org_id":{"type":"string","format":"uuid","title":"Org Id"},"referred_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referred Name"},"referred_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referred Title"},"referred_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referred Email"},"referred_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referred Phone"},"quote":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quote"},"origin":{"type":"string","title":"Origin","default":"human"}},"type":"object","required":["from_contact_id","org_id"],"title":"RecordReferralRequest"},"RecordRefinement":{"properties":{"summary":{"type":"string","title":"Summary"},"model":{"type":"string","title":"Model"},"learned_from":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Learned From","default":[]},"vertical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vertical"}},"type":"object","required":["summary","model"],"title":"RecordRefinement"},"RecordSendRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"playbook_id":{"type":"string","format":"uuid","title":"Playbook Id"},"playbook_version_id":{"type":"string","format":"uuid","title":"Playbook Version Id"},"play":{"type":"string","title":"Play"},"copy_variant":{"type":"string","title":"Copy Variant"},"segment":{"type":"string","title":"Segment"},"signal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Signal Id"},"channel":{"type":"string","title":"Channel"},"adp_band":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adp Band"},"status":{"type":"string","title":"Status","default":"sent"},"to_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To Address"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"},"campaign":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign"}},"type":"object","required":["org_id","playbook_id","playbook_version_id","play","copy_variant","segment","channel"],"title":"RecordSendRequest"},"RecordTransactionRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"quantity_delta":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Quantity Delta"},"reference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference"}},"type":"object","required":["kind","quantity_delta"],"title":"RecordTransactionRequest"},"RefusalOut":{"properties":{"function_key":{"type":"string","title":"Function Key"},"because":{"type":"string","title":"Because"},"deep_link_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deep Link Path"},"deep_link_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deep Link Label"}},"type":"object","required":["function_key","because"],"title":"RefusalOut"},"RegisterBody":{"properties":{"equipment_id":{"type":"string","format":"uuid","title":"Equipment Id"}},"type":"object","required":["equipment_id"],"title":"RegisterBody"},"RejectProposalRequest":{"properties":{},"type":"object","title":"RejectProposalRequest"},"RelaunchRequest":{"properties":{"new_name":{"type":"string","maxLength":200,"minLength":1,"title":"New Name"},"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"}},"type":"object","required":["new_name","reason"],"title":"RelaunchRequest"},"RemitRequest":{"properties":{"board_reviews":{"type":"boolean","title":"Board Reviews"}},"type":"object","required":["board_reviews"],"title":"RemitRequest","description":"Whether the board decides this class."},"RemoveBlockBody":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"RemoveBlockBody","description":"Why a manager is taking the block off, in their own words. Trimmed and\nchecked against `domain.MIN_REMOVAL_REASON_CHARS` by the service (a 422 with\na sentence), so the browser and the API cannot disagree about the minimum."},"ReportBody":{"properties":{"status":{"type":"string","title":"Status"},"device_job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Job Id"},"device_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device State"},"failure":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure"}},"type":"object","required":["status"],"title":"ReportBody"},"ReportFindingRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"subject":{"type":"string","title":"Subject"},"detail":{"type":"string","title":"Detail"},"reference":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference"},"urgent":{"type":"boolean","title":"Urgent","default":false}},"type":"object","required":["kind","subject","detail"],"title":"ReportFindingRequest"},"ReportResolutionRequest":{"properties":{"reference":{"type":"string","title":"Reference"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["reference","detail"],"title":"ReportResolutionRequest"},"ReporterRejectedFixRequest":{"properties":{"note":{"type":"string","title":"Note"}},"type":"object","required":["note"],"title":"ReporterRejectedFixRequest"},"ReprintOrderRequest":{"properties":{"reason":{"type":"string","title":"Reason"}},"type":"object","required":["reason"],"title":"ReprintOrderRequest"},"RequestRepairBody":{"properties":{"repair_name":{"type":"string","title":"Repair Name"},"params":{"additionalProperties":true,"type":"object","title":"Params","default":{}},"issue_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Issue Id"},"reason":{"type":"string","title":"Reason"},"urgent":{"type":"boolean","title":"Urgent","default":false},"ordered_by_chairman_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ordered By Chairman Note"}},"type":"object","required":["repair_name","reason"],"title":"RequestRepairBody"},"ResearchDraftRequest":{"properties":{"text":{"type":"string","minLength":1,"title":"Text"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"},"proposed_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Status"},"proposed_status_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposed Status Reason"}},"type":"object","required":["text"],"title":"ResearchDraftRequest"},"ResearchReplyRequest":{"properties":{"body":{"type":"string","maxLength":8000,"minLength":1,"title":"Body"}},"type":"object","required":["body"],"title":"ResearchReplyRequest"},"ResearchSayRequest":{"properties":{"body":{"type":"string","maxLength":8000,"minLength":1,"title":"Body"}},"type":"object","required":["body"],"title":"ResearchSayRequest"},"ResendSendBody":{"properties":{"ticket_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Ticket Id"},"call_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Call Id"},"request_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Request Id"},"suggestion_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Suggestion Id"},"contact_reason":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Contact Reason"},"reason":{"type":"string","maxLength":1000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"ResendSendBody"},"ResendTransactionalRequest":{"properties":{"message_id":{"type":"string","format":"uuid","title":"Message Id"},"idempotency_key":{"type":"string","title":"Idempotency Key"}},"type":"object","required":["message_id","idempotency_key"],"title":"ResendTransactionalRequest"},"ResolveBlockRequest":{"properties":{"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"}},"type":"object","title":"ResolveBlockRequest"},"ResolveBody":{"properties":{"parcel_code":{"type":"string","maxLength":60,"minLength":1,"title":"Parcel Code"},"box_label_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Box Label Id"}},"type":"object","required":["parcel_code"],"title":"ResolveBody"},"RespondRequest":{"properties":{"rating":{"type":"string","title":"Rating"}},"type":"object","required":["rating"],"title":"RespondRequest","description":"`{ rating: RatingWire }` -- his one click, after he answers."},"RestartAfterErrorRequest":{"properties":{"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","title":"RestartAfterErrorRequest"},"RetentionEventRequest":{"properties":{"org_id":{"type":"string","format":"uuid","title":"Org Id"},"contract_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contract Id"},"event_type":{"type":"string","title":"Event Type"},"decision_date":{"type":"string","format":"date","title":"Decision Date"},"effective_date":{"type":"string","format":"date","title":"Effective Date"},"status":{"type":"string","title":"Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"source_document":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Document"},"exclusion_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Exclusion Reason"}},"type":"object","required":["org_id","event_type","decision_date","effective_date","status"],"title":"RetentionEventRequest"},"ReturnMailBody":{"properties":{"requester_name":{"type":"string","maxLength":200,"minLength":1,"title":"Requester Name"},"requester_email":{"type":"string","maxLength":320,"minLength":3,"title":"Requester Email"},"requester_phone":{"type":"string","maxLength":40,"title":"Requester Phone","default":""},"sender_name":{"type":"string","maxLength":200,"minLength":1,"title":"Sender Name"},"sender_address":{"$ref":"#/components/schemas/Address"},"recipient_name":{"type":"string","maxLength":200,"minLength":1,"title":"Recipient Name"},"recipient_address":{"$ref":"#/components/schemas/Address"},"recipient_id_number":{"type":"string","maxLength":60,"title":"Recipient Id Number","default":""},"facility_name":{"type":"string","maxLength":300,"title":"Facility Name","default":""},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"facility_code":{"type":"string","maxLength":16,"title":"Facility Code","default":""},"mailed_on":{"type":"string","format":"date","title":"Mailed On"},"parcel_description":{"type":"string","maxLength":1000,"title":"Parcel Description","default":""},"handling_choice":{"type":"string","title":"Handling Choice","default":"return_to_sender"},"signature":{"anyOf":[{"type":"string","maxLength":700000},{"type":"null"}],"title":"Signature"},"signature_name":{"type":"string","maxLength":200,"title":"Signature Name","default":""},"company_website":{"type":"string","maxLength":300,"title":"Company Website","default":""},"form_started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Form Started At"},"turnstile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turnstile Token"}},"type":"object","required":["requester_name","requester_email","sender_name","recipient_name","mailed_on"],"title":"ReturnMailBody"},"ReverseStopBody":{"properties":{"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"ReverseStopBody"},"ReviewContactRoleRequest":{"properties":{"decision":{"type":"string","title":"Decision"}},"type":"object","required":["decision"],"title":"ReviewContactRoleRequest"},"ReviewRequest":{"properties":{"decision":{"type":"string","title":"Decision"}},"type":"object","required":["decision"],"title":"ReviewRequest"},"RevokeBody":{"properties":{"step":{"type":"string","title":"Step"},"reason":{"type":"string","title":"Reason","default":""}},"type":"object","required":["step"],"title":"RevokeBody"},"RingBackResult":{"properties":{"call_uuid":{"type":"string","title":"Call Uuid"},"customer_number_ends":{"type":"string","title":"Customer Number Ends"},"caller_id":{"type":"string","title":"Caller Id"}},"type":"object","required":["call_uuid","customer_number_ends","caller_id"],"title":"RingBackResult"},"RuleDecisionRequest":{"properties":{"disposition":{"type":"string","title":"Disposition"},"rule_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rule Text"}},"type":"object","required":["disposition"],"title":"RuleDecisionRequest","description":"`{ disposition: RuleDisposition; rule_text: string | null }` --\n`decisions.ts`'s `decideRule` body. `disposition` is validated against\n`domain.RULE_DISPOSITIONS` inside `service.decide_rule`, not here, so the\none 422 message a caller sees says what a decision disposition IS rather\nthan reciting a Pydantic enum error."},"RunIn":{"properties":{"answering_model":{"type":"string","minLength":1,"title":"Answering Model"},"models_seen":{"items":{"type":"string"},"type":"array","title":"Models Seen"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"},"usage":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Usage"},"duration_ms":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Duration Ms"},"total_cost_usd":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Total Cost Usd"}},"type":"object","required":["answering_model"],"title":"RunIn"},"RunReport":{"properties":{"answering_model":{"type":"string","title":"Answering Model"},"models_seen":{"items":{"type":"string"},"type":"array","title":"Models Seen"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"}},"type":"object","required":["answering_model"],"title":"RunReport","description":"What the seat reports about the run that produced this answer.\n\n⛔`answering_model` AND `models_seen` ARE TWO DIFFERENT FACTS, AND SENDING\nONLY THE SECOND WOULD MAKE THE RULE UNCHECKABLE. `answering_model` is the\nmodel that produced THE ANSWER; `models_seen` is every model the run's\nusage report mentioned, side calls included. The Claude Code CLI bills its\nown side calls on smaller models -- `claude-haiku-4-5` beside\n`claude-fable-5-1` on a one-line call, measured -- so the second list is\nexpected to contain smaller models and is recorded, never judged."},"RunnerStatusRequest":{"properties":{"state":{"type":"string","title":"State"},"detail":{"type":"string","maxLength":1000,"minLength":1,"title":"Detail"}},"type":"object","required":["state","detail"],"title":"RunnerStatusRequest","description":"What a seat's runner says about itself: one of\n`runner_status.STATES`, and the true reason in words. Never a credential."},"SafetyStopBody":{"properties":{"what_happened":{"type":"string","maxLength":2000,"minLength":1,"title":"What Happened"}},"type":"object","required":["what_happened"],"title":"SafetyStopBody"},"SaveDraftRequest":{"properties":{"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"}},"type":"object","title":"SaveDraftRequest"},"SaveTemplateRequest":{"properties":{"subject_template":{"type":"string","title":"Subject Template"},"body_template":{"type":"string","title":"Body Template"}},"type":"object","required":["subject_template","body_template"],"title":"SaveTemplateRequest"},"ScanParcelRequest":{"properties":{"parcel_id":{"type":"string","title":"Parcel Id"},"acknowledge_reprint":{"type":"boolean","title":"Acknowledge Reprint","default":false}},"type":"object","required":["parcel_id"],"title":"ScanParcelRequest","description":"What the scanner produced. A STRING, not a UUID.\n\nThis was `parcel_id: UUID`, which meant no real parcel QR could ever be\nscanned: the QR encodes v1's full_url\n(https://www.pigeonlycorrections.com/p/8P5XYY97), and a technician typing\nby hand enters the 8 characters printed on the label. Pydantic rejected\nboth before the request reached a lookup — \"Input should be a valid UUID,\ninvalid character: found `h` at 1\", the h of https.\n\nStill accepts a UUID, because internal callers and older clients send\none; resolve_scanned_parcel tries the code first, then that."},"SeatAnswerFramingRequest":{"properties":{"seat_key":{"type":"string","title":"Seat Key"},"framing_notes":{"type":"string","title":"Framing Notes"},"added_options":{"items":{"$ref":"#/components/schemas/OptionIn"},"type":"array","title":"Added Options","default":[]}},"type":"object","required":["seat_key","framing_notes"],"title":"SeatAnswerFramingRequest","description":"A seat's blind read, as the chair received it."},"SeatAnswerPositionsRequest":{"properties":{"seat_key":{"type":"string","title":"Seat Key"},"positions":{"items":{"$ref":"#/components/schemas/PositionIn"},"type":"array","title":"Positions"}},"type":"object","required":["seat_key","positions"],"title":"SeatAnswerPositionsRequest","description":"A seat's vote, as the chair received it."},"SeatCallRequest":{"properties":{"lane":{"type":"string","maxLength":64,"minLength":1,"title":"Lane"},"provider":{"type":"string","maxLength":64,"minLength":1,"title":"Provider"},"model":{"type":"string","maxLength":160,"minLength":1,"title":"Model"},"ok":{"type":"boolean","title":"Ok"},"latency_ms":{"type":"integer","maximum":86400000.0,"minimum":0.0,"title":"Latency Ms"},"turns":{"anyOf":[{"type":"integer","maximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Turns"},"input_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Input Tokens"},"cache_creation_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Creation Tokens"},"cache_read_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Cache Read Tokens"},"output_tokens":{"anyOf":[{"type":"integer","maximum":2000000000.0,"minimum":0.0},{"type":"null"}],"title":"Output Tokens"},"error":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Error"}},"type":"object","required":["lane","provider","model","ok","latency_ms"],"title":"SeatCallRequest","description":"One `claude` run, as `tools/rooster_worker/claude_cli.py` saw it. Every\ncount is the CLI's own `--output-format json` usage block; None means the\nrun reported none (timed out, could not start, plain-text output)."},"SeatFailedRequest":{"properties":{"reason":{"type":"string","title":"Reason"},"paused":{"type":"boolean","title":"Paused","default":false}},"type":"object","required":["reason"],"title":"SeatFailedRequest"},"SeatIsOutRequest":{"properties":{"seat_key":{"type":"string","title":"Seat Key"},"stage":{"type":"string","title":"Stage","default":"framing"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["seat_key","detail"],"title":"SeatIsOutRequest","description":"The seat could not run its top model for this paper at all."},"SeatOutIn":{"properties":{"detail":{"type":"string","maxLength":2000,"minLength":1,"title":"Detail"}},"type":"object","required":["detail"],"title":"SeatOutIn"},"SeatPrincipalRequest":{"properties":{"principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Principal Id"}},"type":"object","required":["principal_id"],"title":"SeatPrincipalRequest","description":"Who answers for a seat through its own tool; null unbinds."},"SeatProbeResultReport":{"properties":{"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"},"detail":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Detail"},"credential":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Credential"}},"type":"object","title":"SeatProbeResultReport","description":"What the container's own Claude Code seat probe (`apps/rooster/\nseat_probe.py`) found, on every boot and every 60-second watch pass --\nthe subscription setup token only; there is no metered fallback (see\nthat module's own docstring)."},"SendEditedDraftRequest":{"properties":{"text":{"type":"string","title":"Text"},"channel":{"type":"string","title":"Channel","default":"email"},"target_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Status"},"cc_emails":{"items":{"type":"string"},"type":"array","title":"Cc Emails"}},"type":"object","required":["text"],"title":"SendEditedDraftRequest"},"SendIn":{"properties":{"send_id":{"type":"string","format":"uuid","title":"Send Id"},"to_email":{"type":"string","maxLength":320,"title":"To Email"},"cc_emails":{"items":{"type":"string"},"type":"array","maxItems":10,"title":"Cc Emails"},"subject":{"type":"string","maxLength":300,"title":"Subject"},"body":{"type":"string","maxLength":20000,"title":"Body"}},"type":"object","required":["send_id","to_email","subject","body"],"title":"SendIn"},"SendMessageRequest":{"properties":{"target_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Status"},"deliberate_retry":{"type":"boolean","title":"Deliberate Retry","default":false}},"type":"object","title":"SendMessageRequest","description":"`target_status`: the human's OWN explicit outcome choice for this\nticket, made in the SAME request as the send (TicketDetailPage.tsx's\n\"Choose the outcome status\" composer control) -- e.g. \"Resolved\". None\n(the field's default, and every body-less request from a client that\npredates this) falls through to service.send_message's own default: move\na new/open ticket to waiting_on_customer automatically. Optional, and the\nwhole body is optional below, so a bare `POST .../send` with no body at\nall -- every caller before 2026-08-07 -- is unaffected."},"SendRunRequest":{"properties":{"first":{"items":{"type":"string"},"type":"array","title":"First","default":[]},"products":{"items":{"type":"string"},"type":"array","title":"Products","default":[]},"source":{"type":"string","title":"Source","default":"all"},"received_from":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Received From"},"received_to":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Received To"},"limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"},"group":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group"},"expect_orders":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expect Orders"},"test":{"type":"boolean","title":"Test","default":false}},"type":"object","title":"SendRunRequest"},"SendTestRequest":{"properties":{"test_email":{"type":"string","title":"Test Email"}},"type":"object","required":["test_email"],"title":"SendTestRequest"},"SentHomeBody":{"properties":{"manager":{"type":"string","title":"Manager"}},"type":"object","required":["manager"],"title":"SentHomeBody"},"SetAllocationRequest":{"properties":{"allocation_weight":{"type":"number","title":"Allocation Weight"}},"type":"object","required":["allocation_weight"],"title":"SetAllocationRequest"},"SetAntiBotRequest":{"properties":{"enabled":{"type":"boolean","title":"Enabled"}},"type":"object","required":["enabled"],"title":"SetAntiBotRequest"},"SetBriefRequest":{"properties":{"brief_text":{"type":"string","title":"Brief Text"},"sensitivity":{"type":"string","title":"Sensitivity","default":"normal"}},"type":"object","required":["brief_text"],"title":"SetBriefRequest"},"SetContactRolesRequest":{"properties":{"roles":{"items":{"$ref":"#/components/schemas/ContactRoleRequest"},"type":"array","title":"Roles"}},"type":"object","required":["roles"],"title":"SetContactRolesRequest"},"SetCountRequest":{"properties":{"counted":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Counted"}},"type":"object","required":["counted"],"title":"SetCountRequest","description":"The number on the shelf -- a count, not a difference. See\nservice.set_counted_quantity."},"SetCredentialRequest":{"properties":{"secret":{"type":"string","minLength":1,"title":"Secret"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["secret"],"title":"SetCredentialRequest","description":"A pasted key. `note` is optional and, when omitted, leaves any existing\nnote alone rather than clearing it."},"SetDisclosurePolicyRequest":{"properties":{"customer_reachable":{"type":"boolean","title":"Customer Reachable"}},"type":"object","required":["customer_reachable"],"title":"SetDisclosurePolicyRequest"},"SetFeatureFlagRequest":{"properties":{"state":{"type":"string","pattern":"^(on|off|percent)$","title":"State"},"percent":{"type":"integer","maximum":100.0,"minimum":0.0,"title":"Percent","default":0}},"type":"object","required":["state"],"title":"SetFeatureFlagRequest"},"SetFloorSchedulingLiveRequest":{"properties":{"live":{"type":"boolean","title":"Live"}},"type":"object","required":["live"],"title":"SetFloorSchedulingLiveRequest"},"SetGateRequest":{"properties":{"gate_pct":{"type":"number","title":"Gate Pct"}},"type":"object","required":["gate_pct"],"title":"SetGateRequest"},"SetGitLabUsernameRequest":{"properties":{"principal_id":{"type":"string","format":"uuid","title":"Principal Id"},"gitlab_username":{"type":"string","minLength":1,"title":"Gitlab Username"}},"type":"object","required":["principal_id","gitlab_username"],"title":"SetGitLabUsernameRequest","description":"Map a Roost principal to their GitLab username -- the operator path\nFrederick's standing rule 4 requires (F10, Fable review of #2196,\n2026-09-25): before this route, `principals.gitlab_username` was\nwritten only by migration 0642's two literal `UPDATE`s (Marc,\nJonathan), and mapping a THIRD person meant a hand `UPDATE` on\nproduction, which house rule makes a `tools/repairs/` data repair, not\nsomething a person types. This is the route; `docs/runbooks/\nGITLAB_WEBHOOK_REGISTRATION.md`'s own sibling doc names the exact\nsteps for using it."},"SetModelPinRequest":{"properties":{"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"effort":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Effort"},"thinking":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thinking"}},"type":"object","title":"SetModelPinRequest","description":"`model=None` clears the pin -- the surface falls back to its\nenv/family/default resolution. `effort` is optional and ignored when\n`model` is None (clearing never carries an effort); when set alongside\na model it is written as the `{\"model\", \"effort\"}` pin shape\n(COMPANY-OS-DECISION-LAYER-001 unit 1). See\nsettings_service.set_model_pin."},"SetPacingRequest":{"properties":{"daily_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Daily Cap"},"weekly_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Weekly Cap"}},"type":"object","title":"SetPacingRequest"},"SetParcelCountRequest":{"properties":{"parcel_count":{"type":"integer","minimum":0.0,"title":"Parcel Count"}},"type":"object","required":["parcel_count"],"title":"SetParcelCountRequest","description":"The month's parcel count for a per_parcel draft.\n\nFrederick: \"I have to plug in the data, the number of count, of parcels,\nand we don't know what that is until after the new month.\""},"SetProductTypeRequest":{"properties":{"product_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product Type"}},"type":"object","title":"SetProductTypeRequest","description":"`PUT /{id}/prints`. `product_type` null says the machine prints\nnothing."},"SettleJobRequest":{"properties":{"outcome":{"type":"string","title":"Outcome"},"reason":{"type":"string","title":"Reason"},"evidence":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Evidence"},"line_item_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Line Item Id"}},"type":"object","required":["outcome","reason"],"title":"SettleJobRequest"},"SettleRunRequest":{"properties":{"outcome":{"type":"string","title":"Outcome"},"after":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"After"}},"type":"object","required":["outcome"],"title":"SettleRunRequest"},"ShipBoxBody":{"properties":{"box_size":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Box Size"},"weight_oz":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weight Oz"},"ship_date":{"type":"string","title":"Ship Date"},"leftover_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Leftover Reason"}},"type":"object","required":["ship_date"],"title":"ShipBoxBody","description":"Close a box by buying its postage."},"SignCSOfferBody":{"properties":{"signature_data_url":{"type":"string","title":"Signature Data Url"}},"type":"object","required":["signature_data_url"],"title":"SignCSOfferBody"},"SortCountBody":{"properties":{"date":{"type":"string","format":"date","title":"Date"},"rts":{"type":"integer","title":"Rts"},"dlo":{"type":"integer","title":"Dlo"}},"type":"object","required":["date","rts","dlo"],"title":"SortCountBody","description":"Frederick, 2026-09-14: \"We should have an RTS box, a dead letter\nbox. Everything else can be derived.\" Two numbers; live, mail quantity\nand the sort's minutes are counted server-side (service._row_to_sort)."},"StagedFixRequest":{"properties":{"pr_url":{"type":"string","title":"Pr Url"},"branch":{"type":"string","title":"Branch"},"summary":{"type":"string","title":"Summary"},"how_to_check":{"type":"string","title":"How To Check"},"outcome":{"type":"string","title":"Outcome","default":""},"still_off":{"type":"string","title":"Still Off","default":""},"upstream_answer":{"type":"string","title":"Upstream Answer","default":""}},"type":"object","required":["pr_url","branch","summary","how_to_check"],"title":"StagedFixRequest"},"StartEmailTicketRequest":{"properties":{"subject":{"type":"string","title":"Subject","default":""},"body":{"type":"string","title":"Body"},"idempotency_key":{"type":"string","format":"uuid","title":"Idempotency Key"}},"type":"object","required":["body","idempotency_key"],"title":"StartEmailTicketRequest","description":"The email a person typed on a phone ticket: it goes out from a NEW\nemail ticket, never from the phone ticket. `subject` may be blank (the\nphone ticket's subject is used); `body` may not."},"StartEquipmentConversationRequest":{"properties":{"label_id":{"type":"string","title":"Label Id"}},"type":"object","required":["label_id"],"title":"StartEquipmentConversationRequest"},"StartIdCheckBody":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"birth_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Birth Date"}},"type":"object","required":["name"],"title":"StartIdCheckBody","description":"What the contractor typed, to compare with what Vouched reads off the\nID: their name exactly as it is on the ID, and their date of birth."},"StartPhoneTicketRequest":{"properties":{"number":{"type":"string","title":"Number"}},"type":"object","required":["number"],"title":"StartPhoneTicketRequest","description":"The number a person picked from the customer's contact record. It is\nmatched against the record's own numbers, never dialled as typed: a number\nthat is not on the record is refused (Frederick, 2026-10-07, issue #2002:\n\"Yes. The number on a customer's contact record should be enough.\")."},"StatusIn":{"properties":{"status":{"type":"string","title":"Status"},"reason":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Reason"},"signed_pdf_base64":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Signed Pdf Base64"}},"type":"object","required":["status"],"title":"StatusIn"},"SteerRequest":{"properties":{"words":{"type":"string","maxLength":300,"minLength":1,"title":"Words"}},"type":"object","required":["words"],"title":"SteerRequest","description":"What a manager typed in the Printing page's steering box, as typed."},"StickerBody":{"properties":{"code":{"type":"string","title":"Code"}},"type":"object","required":["code"],"title":"StickerBody"},"StillOnThePressBody":{"properties":{"job_ids":{"items":{"type":"string","format":"uuid"},"type":"array","maxItems":5000,"title":"Job Ids"}},"type":"object","required":["job_ids"],"title":"StillOnThePressBody"},"SubmitOrderRequest":{"properties":{"centrics_order_id":{"type":"string","title":"Centrics Order Id"},"retailer_order_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Retailer Order Number"},"source_service":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Service"},"retailer_job_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Retailer Job Number"},"recipient":{"additionalProperties":true,"type":"object","title":"Recipient"},"sender":{"additionalProperties":true,"type":"object","title":"Sender"},"shipping_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Shipping Method"},"shipping_carrier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Shipping Carrier"},"retailer_affiliate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Retailer Affiliate"},"manual_review":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Manual Review"},"qr_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Qr Code"},"manufacturing_priority":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manufacturing Priority"},"retailer_order_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Retailer Order Date"},"line_items":{"items":{"$ref":"#/components/schemas/LineItem"},"type":"array","title":"Line Items"}},"type":"object","required":["centrics_order_id","line_items"],"title":"SubmitOrderRequest"},"SuppressRequest":{"properties":{"identifier_type":{"type":"string","title":"Identifier Type"},"identifier":{"type":"string","title":"Identifier"},"reason":{"type":"string","title":"Reason"},"source":{"type":"string","title":"Source","default":"manual"}},"type":"object","required":["identifier_type","identifier","reason"],"title":"SuppressRequest"},"SwapBody":{"properties":{"taker_contractor_ids":{"anyOf":[{"items":{"type":"string","format":"uuid"},"type":"array"},{"type":"null"}],"title":"Taker Contractor Ids"}},"type":"object","title":"SwapBody"},"SwapOffer":{"properties":{"shift_id":{"type":"string","format":"uuid","title":"Shift Id"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["shift_id"],"title":"SwapOffer"},"TeamPayIn":{"properties":{"employment_type":{"type":"string","title":"Employment Type"},"amount":{"type":"string","title":"Amount"},"expected_weekly_hours":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expected Weekly Hours"},"work_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Work State"}},"type":"object","required":["employment_type","amount"],"title":"TeamPayIn"},"TechnicianRequiredRequest":{"properties":{"statement":{"type":"string","title":"Statement"},"expected_repair_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expected Repair At"}},"type":"object","required":["statement"],"title":"TechnicianRequiredRequest","description":"A floor manager's own words that this machine needs a technician\n(issue #2958, FLOOR-GPS-SCHEDULER-001 S10) -- see `equipment.\ntechnician_override.record_technician_required`."},"TelemetryAlert":{"properties":{"code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Code"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","title":"TelemetryAlert"},"TelemetryBody":{"properties":{"host":{"type":"string","title":"Host"},"reachable":{"type":"boolean","title":"Reachable","default":true},"device_status":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Device Status"},"printer_status":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Printer Status"},"error_state_bits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error State Bits"},"alerts":{"items":{"$ref":"#/components/schemas/TelemetryAlert"},"type":"array","title":"Alerts","default":[]},"lifetime_pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lifetime Pages"},"supplies":{"additionalProperties":true,"type":"object","title":"Supplies","default":{}},"serial_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial Number"},"agent_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version"},"counters":{"items":{"$ref":"#/components/schemas/TelemetryCounter"},"type":"array","title":"Counters","default":[]}},"type":"object","required":["host"],"title":"TelemetryBody","description":"One SNMP read of one machine (the Ricoh engine or its Fiery\ncontroller), from `print_agent.py`'s own SNMP client -- see that\nmodule's `snmp_get`/`poll_ricoh` and docs/runbooks/RICOH_LIVE_LINK.md.\n\n`host` is the exact ipv4 the agent polled: this is how\n`equipment.telemetry.ingest_snapshot` resolves WHICH equipment row this\nis about (`equipment_endpoints`, migration 0412), never a hardcoded\nmachine id the agent would have to know."},"TelemetryCounter":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Value"}},"type":"object","title":"TelemetryCounter","description":"One row of the Ricoh's own private counter table\n(1.3.6.1.4.1.367.3.2.1.2.19.5.1: column 5 = name, column 9 = value) --\nsee docs/runbooks/RICOH_METER_COUNTERS.md. `equipment.meter_history.\nmap_counters` decides which name means color vs black-and-white;\nthis route forwards the raw list untouched."},"TextReplyRequest":{"properties":{"text":{"type":"string","title":"Text"},"target_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Status"}},"type":"object","required":["text"],"title":"TextReplyRequest"},"TheirClockResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning"},"place":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Place"},"their_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Their Time"}},"type":"object","required":["ok"],"title":"TheirClockResponse","description":"What the dialpad shows before a manual dial at an odd hour."},"ThroughputOverrideRequest":{"properties":{"rated_throughput_per_hour":{"type":"integer","title":"Rated Throughput Per Hour"},"throughput_unit":{"type":"string","title":"Throughput Unit"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["rated_throughput_per_hour","throughput_unit","reason"],"title":"ThroughputOverrideRequest"},"TicketSatisfactionRequest":{"properties":{"rating":{"type":"integer","title":"Rating"},"rated_by":{"type":"string","title":"Rated By"}},"type":"object","required":["rating","rated_by"],"title":"TicketSatisfactionRequest"},"TimeOffDecision":{"properties":{"state":{"type":"string","title":"State"},"note":{"type":"string","title":"Note"}},"type":"object","required":["state","note"],"title":"TimeOffDecision"},"TimeOffRequest":{"properties":{"first_day":{"type":"string","format":"date","title":"First Day"},"last_day":{"type":"string","format":"date","title":"Last Day"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["first_day","last_day"],"title":"TimeOffRequest","description":"Days somebody is asking for, and why if they want to say."},"TrainingCreateBody":{"properties":{"team":{"type":"string","enum":["fulfillment","customer_service"],"title":"Team"},"trainees":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Trainees"},"trainer":{"type":"string","format":"uuid","title":"Trainer"},"kind":{"type":"string","enum":["new_hire","retraining"],"title":"Kind"},"from":{"type":"string","format":"date","title":"From"},"to":{"type":"string","format":"date","title":"To"}},"type":"object","required":["team","trainees","trainer","kind","from","to"],"title":"TrainingCreateBody","description":"`POST /api/workforce/training`'s own wire shape, CHANGES.md §2,\nverbatim: `{ team, trainees: [id], trainer: id, kind: 'new_hire' |\n'retraining', from: date, to: date }`. `from` is a Python keyword, so\nthe field is `from_` with the wire name restored via the alias."},"TrainingWindowBody":{"properties":{"trainee_principal_id":{"type":"string","format":"uuid","title":"Trainee Principal Id"},"trainer_principal_id":{"type":"string","format":"uuid","title":"Trainer Principal Id"},"first_day":{"type":"string","format":"date","title":"First Day"},"last_day":{"type":"string","format":"date","title":"Last Day"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["trainee_principal_id","trainer_principal_id","first_day","last_day","reason"],"title":"TrainingWindowBody"},"TransferTicketRequest":{"properties":{"to_kind":{"type":"string","title":"To Kind"},"note":{"type":"string","title":"Note"}},"type":"object","required":["to_kind","note"],"title":"TransferTicketRequest","description":"The \"move this to the other department\" action (Frederick: \"there\nare sometimes when a b2g ticket needs to be transferred over to a b2c,\nand times when a b2c has to be transferred over to b2g ... the user\nshould make an internal note, and they should be allowed to reassign\nthat ticket to the other queue\"). `to_kind` is required and must name\nthe ticket's OTHER kind -- explicit, not inferred, the same \"the client\nconfirms what it is asking for\" discipline TransitionRequest's own\n`to_status` uses, so a stale client cannot silently transfer a ticket\nthat already moved out from under it. `note` is required and refused\nempty (service.TicketTransferNoteRequired) -- Frederick named this\nfirst, and it is the receiving department's only record of why the\nticket is suddenly in their pool."},"TransitionIssueRequest":{"properties":{"to_status":{"type":"string","title":"To Status"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"disposition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition"},"attachment_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Attachment Ids","default":[]}},"type":"object","required":["to_status"],"title":"TransitionIssueRequest"},"TransitionRequest":{"properties":{"to_status":{"type":"string","title":"To Status"}},"type":"object","required":["to_status"],"title":"TransitionRequest"},"TriageTicketRequest":{"properties":{"outcome":{"type":"string","title":"Outcome"},"reason":{"type":"string","title":"Reason","default":""}},"type":"object","required":["outcome"],"title":"TriageTicketRequest"},"TriggerMakeGoodRequest":{"properties":{"apology_text":{"type":"string","title":"Apology Text"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"agent_provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Provider"},"agent_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Model"},"agent_harness":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Harness"}},"type":"object","required":["apology_text"],"title":"TriggerMakeGoodRequest"},"TubCountBody":{"properties":{"date":{"type":"string","format":"date","title":"Date"},"deep":{"type":"integer","title":"Deep"},"shallow":{"type":"integer","title":"Shallow"}},"type":"object","required":["date","deep","shallow"],"title":"TubCountBody"},"TubsBody":{"properties":{"deep":{"type":"integer","title":"Deep"},"shallow":{"type":"integer","title":"Shallow"}},"type":"object","required":["deep","shallow"],"title":"TubsBody"},"UndoRequest":{"properties":{"undo_token":{"type":"string","format":"uuid","title":"Undo Token"}},"type":"object","required":["undo_token"],"title":"UndoRequest"},"UpdateConstitutionRequest":{"properties":{"agent_act_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Agent Act Allowed"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","title":"UpdateConstitutionRequest"},"UpdateContactRequest":{"properties":{"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"role_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role Title"},"is_champion":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Champion"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"clear_email":{"type":"boolean","title":"Clear Email","default":false},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"mailing_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mailing Address"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"departed_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Departed Note"},"is_distribution_list":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Distribution List"},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"clear_facility":{"type":"boolean","title":"Clear Facility","default":false}},"type":"object","title":"UpdateContactRequest"},"UpdateContractRequest":{"properties":{"contract_type":{"type":"string","title":"Contract Type"},"pricing_model":{"type":"string","title":"Pricing Model"},"starts_on":{"type":"string","format":"date","title":"Starts On"},"term_no":{"type":"integer","title":"Term No"},"facility_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Facility Id"},"billing_interval":{"type":"string","title":"Billing Interval","default":"monthly"},"billing_timing":{"type":"string","title":"Billing Timing","default":"in_advance"},"payment_terms":{"type":"string","title":"Payment Terms","default":"net_30"},"payment_method":{"type":"string","title":"Payment Method","default":"check"},"billable_mail_types":{"type":"string","title":"Billable Mail Types","default":"physical_only"},"status":{"type":"string","title":"Status","default":"active"},"adp_count":{"type":"integer","title":"Adp Count","default":0},"rate_per_adp":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Rate Per Adp"},"monthly_minimum":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Monthly Minimum"},"rate_per_parcel":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Rate Per Parcel"},"flat_amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Flat Amount"},"ends_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Ends On"},"auto_renew":{"type":"boolean","title":"Auto Renew","default":true},"sales_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Sales Principal Id"},"vp_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Vp Principal Id"},"commission_from":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Commission From"},"signed_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Signed On"},"deal_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Source"},"selection_route":{"type":"string","title":"Selection Route"},"payment_mechanism":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Mechanism"},"opportunity_origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opportunity Origin"},"special_commission_rate":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Special Commission Rate"}},"type":"object","required":["contract_type","pricing_model","starts_on","term_no","selection_route"],"title":"UpdateContractRequest","description":"A full replace of an existing term line (Contract Terms rebuild\nstage 3's guided entry form edit mode) — every field the guided form\nshows is required here except the handful that are genuinely optional\non a term line (facility, the pricing fields the chosen model does not\nuse, the commission fields a renewal does not carry). `source` is\ndeliberately absent: how a line came to exist is never editable."},"UpdateDealRequest":{"properties":{"amount_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Cents"},"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"close_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Close Date"},"deal_size":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Size"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"},"originated_by_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Originated By Principal Id"}},"type":"object","title":"UpdateDealRequest","description":"Edit a deal in place -- above all price and ADP, which move while a\ndeal is open. Same omitted-vs-null rule as UpdateLeadRequest."},"UpdateEngineSettingsRequest":{"properties":{"supervised":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Supervised"},"pace":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pace"},"adp_autonomy_threshold":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp Autonomy Threshold"},"segment_focus":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Segment Focus"}},"type":"object","title":"UpdateEngineSettingsRequest"},"UpdateFacilityMailingAddressRequest":{"properties":{"address":{"type":"string","title":"Address"}},"type":"object","required":["address"],"title":"UpdateFacilityMailingAddressRequest"},"UpdateFacilityRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address"}},"type":"object","title":"UpdateFacilityRequest"},"UpdateInvoiceRequest":{"properties":{"invoice_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Invoice Date"},"due_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Due Date"},"billing_period_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Billing Period Start"},"billing_period_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Billing Period End"},"amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Amount"},"total_amount":{"anyOf":[{"type":"number"},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Amount"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"line_items":{"anyOf":[{"items":{"$ref":"#/components/schemas/LineItemBody"},"type":"array"},{"type":"null"}],"title":"Line Items"},"period_explicitly_set":{"type":"boolean","title":"Period Explicitly Set","default":false}},"type":"object","title":"UpdateInvoiceRequest"},"UpdateLeadRequest":{"properties":{"adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Adp"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"originated_by_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Originated By Principal Id"},"owner_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Owner Principal Id"}},"type":"object","title":"UpdateLeadRequest","description":"Edit a lead in place. Frederick, 2026-08-04: \"You should be able to\nedit leads and deal cards... there might be a difference in pricing --\nyou'd have to update the new pricing. There might be a difference in ADP.\"\n\nEvery field is Optional and OMISSION MEANS UNCHANGED. `est_value_cents`\nis not here (see CreateLeadRequest's own note) -- editing `adp` recomputes\nit server-side (pipeline_service.update_lead)."},"UpdateOrganizationRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"org_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Org Type"},"jurisdiction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"lifecycle_stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lifecycle Stage"},"billing_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Type"},"org_adp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Org Adp"},"account_manager_principal_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Account Manager Principal Id"},"billing_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Email"},"clear_billing_email":{"type":"boolean","title":"Clear Billing Email","default":false},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"agency_level":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agency Level"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"},"address1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address1"},"address2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"short_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Name"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"mail_scanner_vendor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Scanner Vendor"},"mail_scanner_vendor_contract_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Mail Scanner Vendor Contract End"},"billing_partner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Partner Name"},"billing_partner_contact":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Partner Contact"},"billing_cc_emails":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Cc Emails"},"tara_auto_send":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Tara Auto Send"}},"type":"object","title":"UpdateOrganizationRequest"},"UpdatePrincipalRequest":{"properties":{"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"UpdatePrincipalRequest"},"UpdateProfileRequest":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"preferred_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preferred Name"},"birth_month":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Birth Month"},"birth_day":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Birth Day"},"cell_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cell Phone"},"emergency_contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emergency Contact Name"},"emergency_contact_relationship":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emergency Contact Relationship"},"emergency_contact_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Emergency Contact Phone"},"work_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Work Location"},"work_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Work State"},"work_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Work Country"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"tshirt_size":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tshirt Size"},"dietary_restrictions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dietary Restrictions"},"no_dietary_restrictions":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"No Dietary Restrictions"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"slack_handle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slack Handle"},"job_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Title"}},"type":"object","title":"UpdateProfileRequest","description":"The self-service / admin profile editor's body (migration 0143) --\nevery field in registry.PROFILE_FIELDS, all optional so a caller only\never sends what changed. Deliberately excludes `display_name` (the\nfull/legal name) and `is_active`: those stay on UpdatePrincipalRequest/\nPEOPLE_WRITE above -- legal-name changes and offboarding are an admin\naction, not a self-service one, by design (see update_profile's own\ndocstring)."},"UpdateRequesterRequest":{"properties":{"requester_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester Name"},"requester_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester Email"},"requester_phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requester Phone"}},"type":"object","required":["requester_name","requester_email","requester_phone"],"title":"UpdateRequesterRequest","description":"The Customer panel's \"edit these details\" form (ISS-115, Rene-mar\nMacaballug: \"We need this functionality so we can update customers'\nemail addresses, names, and phone numbers\"). All three fields are\nREQUIRED in the body and each may be explicitly `null` -- the panel\nedits them as one form, and an optional-field shape could not tell\n\"leave the phone number alone\" apart from \"clear the phone number\",\nwhich would make clearing a wrong number unexpressible."},"UpdateRoleRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","title":"UpdateRoleRequest"},"UpdateSettingRequest":{"properties":{"value":{"additionalProperties":true,"type":"object","title":"Value"}},"type":"object","required":["value"],"title":"UpdateSettingRequest"},"UseTheSoftphoneBody":{"properties":{"detail":{"type":"string","title":"Detail"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"}},"type":"object","required":["detail"],"title":"UseTheSoftphoneBody","description":"The 421 a ticket dial answers with when the caller must use the\nsoftphone instead of their own phone: the sentence, plus the number on\nfile so the dialpad it sends the browser to can open already filled in.\nSee `ringing_back.UseTheSoftphone`."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VendorPaymentSettingsIn":{"properties":{"amount_cents":{"type":"integer","exclusiveMinimum":0.0,"title":"Amount Cents"},"interval":{"type":"string","enum":["weekly","monthly"],"title":"Interval"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"notes":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Notes"}},"type":"object","required":["amount_cents","interval"],"title":"VendorPaymentSettingsIn","description":"What Frederick pays a vendor: one amount, one interval, a start date."},"VendorRegistrationIn":{"properties":{"company_name":{"type":"string","maxLength":200,"title":"Company Name"},"company_address":{"type":"string","maxLength":500,"title":"Company Address"},"contact_name":{"type":"string","maxLength":200,"title":"Contact Name"},"contact_email":{"type":"string","maxLength":200,"title":"Contact Email"},"contact_phone":{"type":"string","maxLength":40,"title":"Contact Phone"},"country":{"type":"string","maxLength":2,"title":"Country"},"wise_type":{"type":"string","maxLength":60,"title":"Wise Type"},"answers":{"additionalProperties":{"type":"string"},"type":"object","title":"Answers"}},"type":"object","required":["company_name","company_address","contact_name","contact_email","contact_phone","country","wise_type"],"title":"VendorRegistrationIn"},"VerifyFailedRequest":{"properties":{"issue_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Issue Ids"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["issue_ids","reason"],"title":"VerifyFailedRequest"},"ViewAsRequest":{"properties":{"principal_id":{"type":"string","title":"Principal Id"}},"type":"object","required":["principal_id"],"title":"ViewAsRequest"},"ViewerIn":{"properties":{"display_name":{"type":"string","maxLength":120,"minLength":1,"title":"Display Name"},"code_last_four":{"type":"string","pattern":"^[0-9]{4}$","title":"Code Last Four"}},"type":"object","required":["display_name","code_last_four"],"title":"ViewerIn"},"VoidInvoiceRequest":{"properties":{"force":{"type":"boolean","title":"Force","default":false},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","title":"VoidInvoiceRequest"},"WithdrawInviteBody":{"properties":{"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"WithdrawInviteBody"},"app__modules__finance__debt__routes__VoidIn":{"properties":{"reason":{"type":"string","maxLength":500,"minLength":1,"title":"Reason"},"whole_group":{"type":"boolean","title":"Whole Group","default":false}},"type":"object","required":["reason"],"title":"VoidIn"},"app__modules__finance__obligations__incoming_routes__VoidIn":{"properties":{"reason":{"type":"string","maxLength":500,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"VoidIn"},"app__modules__issues__routes__ConverseRequest":{"properties":{"conversation_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Conversation Id"},"message":{"type":"string","title":"Message"},"attachment_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Attachment Ids","default":[]}},"type":"object","required":["message"],"title":"ConverseRequest"},"app__modules__org_documents__routes__VoidIn":{"properties":{"reason":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Reason"}},"type":"object","title":"VoidIn"},"app__modules__sales__authoring_routes__ConverseRequest":{"properties":{"conversation_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Conversation Id"},"playbook_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Playbook Id"},"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"ConverseRequest"},"app__modules__transcription__routes__DispositionRequest":{"properties":{"disposition":{"type":"string","title":"Disposition"},"corrected_fields":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Corrected Fields"},"also_correct":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Also Correct"},"selected_candidate":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Selected Candidate"},"scan_examined":{"type":"boolean","title":"Scan Examined","default":false}},"type":"object","required":["disposition"],"title":"DispositionRequest"},"app__modules__voice__routes__DispositionRequest":{"properties":{"disposition":{"type":"string","title":"Disposition"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"requested_callback_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Requested Callback At"}},"type":"object","required":["disposition"],"title":"DispositionRequest","description":"What the agent says happened, and optionally why in their own words."}}}}