{"openapi":"3.1.0","info":{"title":"Turnout API","description":"Turnout keeps volunteer fire and EMS departments covered. It finds the hours when nobody can respond, fills them by asking the right people and the right neighbours, and interrupts the chief only for the one decision that needs a human.\n\nEvery read endpoint accepts the public sandbox key in an x-api-key header. The demo data is synthetic: Millbrook, Riverton and Cedar Hollow are fictional departments.","version":"1.0.0"},"paths":{"/api/keys":{"post":{"tags":["ops"],"summary":"New Key","description":"Issue a sandbox API key. Anyone may call this, and that is the point.\n\nThe key is signed, not stored, and it expires in a day. It proves nothing about who is calling,\nbecause this is a sandbox over synthetic data that anybody can reset. It exists so the\nauthentication path is something you can exercise rather than something this page claims.","operationId":"new_key_api_keys_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response New Key Api Keys Post"}}}}}}},"/api/health":{"get":{"tags":["ops"],"summary":"Health","description":"Liveness for the status page. Returns the simulated clock so a stuck demo is obvious.","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Api Health Get"}}}}}}},"/api/state":{"get":{"tags":["read"],"summary":"Get State","description":"Everything the Station Board shows: headline, gaps, ledger, members, incidents, interrupts.","operationId":"get_state_api_state_get","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get State Api State Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/gaps":{"get":{"tags":["read"],"summary":"Get Gaps","description":"Coverage gaps for the next seven days, each with the numbers behind its risk score.","operationId":"get_gaps_api_gaps_get","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Gaps Api Gaps Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/messages":{"get":{"tags":["read"],"summary":"Get Messages","description":"Every text sent and received. Pass a phone number for one thread.","operationId":"get_messages_api_messages_get","parameters":[{"name":"phone","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"}},{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Messages Api Messages Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/network":{"get":{"tags":["read"],"summary":"Get Network","description":"The three departments, their mutual aid balances, and every A2A exchange so far.","operationId":"get_network_api_network_get","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Network Api Network Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/crew":{"get":{"tags":["read"],"summary":"Get Crew","description":"Every member, and everything the agent knows and has done about them.\n\nA scheduling agent asks real people for hours of their life. This is the accountability side of\nthat: how often each member has been asked this week, the cap that stops it, the hours it will\nnot text them, every message it sent, and the response history behind the probability it uses\nto choose. Nothing here is a model's opinion.","operationId":"get_crew_api_crew_get","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Crew Api Crew Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/trace":{"get":{"tags":["read"],"summary":"Get Trace","description":"Agent trace events. Pass the previous `next` value to poll for new ones.","operationId":"get_trace_api_trace_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Since"}},{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Trace Api Trace Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/roster/parse":{"post":{"tags":["onboarding"],"summary":"Parse Roster Endpoint","description":"Read a pasted roster and report what was understood, before anything is saved.\n\n    Accepts whatever the chief already has: a group text, a spreadsheet paste, a printed list.\n    Lines it could not read come back in `unreadable` rather than being dropped, because a member\n    missing from the roster is a member the agent will never ask.\n\n    Body: {\"text\": \"Dana Ortiz (Chief) 555-298-6397\nLuis Reyes - driver 555 111 2222\"}","operationId":"parse_roster_endpoint_api_roster_parse_post","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"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 Parse Roster Endpoint Api Roster Parse Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/risk/score":{"post":{"tags":["try it"],"summary":"Score Window Endpoint","description":"Score a coverage window with your own people, and see every number behind the answer.\n\nThe demo answers one question about one fictional department. This is the same engine, pointed\nat whatever you actually have. Nothing is stored.\n\nBody, all optional except `available`:\n\n    {\n      \"window_start\": \"2026-09-10T10:00\",   the window, defaults to the next weekday at 10:00\n      \"hours\": 4,                            how long, defaults to 4\n      \"available\": [                         who could respond\n        {\"roles\": [\"firefighter\"], \"responds\": 0.45},\n        {\"roles\": [\"driver_operator\", \"firefighter\"]}\n      ],\n      \"min_crew\": {\"driver_operator\": 1, \"firefighter\": 2},\n      \"calls_per_day\": 1.6,                  your department's own rate\n      \"weather\": \"ice storm warning\",        or none\n      \"time_critical_share\": 0.65            share of your calls that cannot wait\n    }\n\n`responds` defaults to 0.32, the middle of the 17 to 47 percent range reported for volunteer\nfirst responders. If you know your own rate, pass it.","operationId":"score_window_endpoint_api_risk_score_post","parameters":[{"name":"x-api-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"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 Score Window Endpoint Api Risk Score Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/step":{"post":{"tags":["demo"],"summary":"Post Step","description":"Run the next step of the demo week, or a named one via {\"step\": \"neighbors\"}.","operationId":"post_step_api_step_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Step Api Step Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reply":{"post":{"tags":["demo"],"summary":"Post Reply","description":"Deliver an inbound text, exactly as a real phone would.\n\nBody: {\"phone\": \"+1555...\", \"body\": \"1\"}","operationId":"post_reply_api_reply_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 Post Reply Api Reply Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reset":{"post":{"tags":["demo"],"summary":"Post Reset","description":"Put the scenario back to Wednesday 06:30.","operationId":"post_reset_api_reset_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Post Reset Api Reset Post"}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}