{"openapi":"3.1.0","info":{"title":"OnCallRadar API","version":"1.0.0","description":"REST API for an organization. Authenticate with an org API token (created by the org Owner in Settings → Organization) via `Authorization: Bearer <token>`. All responses are JSON and scoped to the token's organization.\n\n**Rate limits.** 60 requests/minute per token and 80/minute per IP. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; a `429` includes `Retry-After`.\n\n**Abuse control.** Exceeding the per-token limit **auto-revokes that token** and puts the organization into a cooldown during which **no new token can be issued**. The cooldown is 15 minutes on the first violation and doubles on each consecutive one (30m, 60m, …); the escalation resets after 24 hours with no further violation."},"servers":[{"url":"/api/v1","description":"This server"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"People","description":"Read people in the organization."},{"name":"Schedule","description":"Read schedules."},{"name":"On call","description":"Who is on call right now."},{"name":"Reporting","description":"Hours & cost reporting."},{"name":"Settings","description":"Organization configuration (e.g. calling capabilities)."},{"name":"Call/Message","description":"Place voice calls / send SMS by escalating a schedule's on-call order."}],"paths":{"/schedules":{"get":{"tags":["Schedule"],"summary":"List schedules","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"}],"responses":{"200":{"description":"A page of schedules","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/schedules/{id}":{"get":{"tags":["Schedule"],"summary":"Get a schedule","parameters":[{"$ref":"#/components/parameters/id"}],"responses":{"200":{"description":"The schedule","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/people":{"get":{"tags":["People"],"summary":"List people","parameters":[{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"}],"responses":{"200":{"description":"A page of people","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/people/{id}":{"get":{"tags":["People"],"summary":"Get a person","parameters":[{"$ref":"#/components/parameters/id"}],"responses":{"200":{"description":"The person","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/reports":{"get":{"tags":["Reporting"],"summary":"Hours & cost report","description":"Hours and cost per person / pay band / schedule, with totals converted to the org default currency.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Range start (ISO). Defaults to the current month."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"Range end (ISO, exclusive)."},{"name":"scope","in":"query","schema":{"type":"string","enum":["all","past","future"]},"description":"Clamp to elapsed / remaining time."},{"name":"scheduleId","in":"query","schema":{"type":"string"}},{"name":"userId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"The report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Report"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/on-call":{"get":{"tags":["On call"],"summary":"Who is on call now","description":"Per schedule, the full contact order in `escalation`: index 0 is whoever is on call now (kind `shift`), then the standing fallback chain (kind `escalation`). Indexes line up with the `index` param of POST /escalations. Plus the next upcoming shift.","responses":{"200":{"description":"On-call snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnCall"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/voice/config":{"get":{"tags":["Settings"],"summary":"Calling configuration","description":"Whether voice calls and/or SMS are configured and enabled for this organization.","responses":{"200":{"description":"Config","content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string"},"configured":{"type":"boolean"},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"}}},"sms":{"type":"object","properties":{"enabled":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/escalations":{"post":{"tags":["Call/Message"],"summary":"Start an escalation","description":"Call/text the on-call list for a schedule. With `index`, page only that one position (no follow-ups to anyone else); omit `index` to cascade the whole order. `auto` prefers voice whenever calling is configured — retrying each person up to `retries` times (default: the org setting) until someone acknowledges — and uses SMS only for an SMS-only org. An SMS escalation messages the first person it can reach and stops (SMS can't be acknowledged). Returns immediately; poll GET /escalations/{id} for the result.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scheduleId"],"properties":{"scheduleId":{"type":"string"},"index":{"type":"integer","minimum":0,"description":"Page ONLY the person at this 0-based on-call position, with no follow-ups to anyone else. Omit to cascade the whole chain."},"channel":{"type":"string","enum":["voice","sms","auto"],"description":"Default auto: prefer voice when configured, else SMS."},"message":{"type":"string","description":"Override the schedule/org default message for this escalation."},"retries":{"type":"integer","minimum":1,"maximum":10,"description":"Voice attempts per person before moving on. Omitted → the org default (escalationMaxAttempts, 3 unless changed). SMS is never retried."}}}}}},"responses":{"200":{"description":"Escalation started","content":{"application/json":{"schema":{"type":"object","properties":{"escalation":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/escalations/{id}":{"get":{"tags":["Call/Message"],"summary":"Escalation status","description":"Progress + result: status, who acknowledged and when, and each call/SMS attempt.","parameters":[{"$ref":"#/components/parameters/id"}],"responses":{"200":{"description":"Escalation","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Org API token (Authorization: Bearer <token>)."}},"parameters":{"id":{"name":"id","in":"path","required":true,"schema":{"type":"string"}},"limit":{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":200}},"offset":{"name":"offset","in":"query","schema":{"type":"integer","default":0,"minimum":0}}},"responses":{"Unauthorized":{"description":"Missing/invalid/revoked token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Invalid parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Too many requests (see Retry-After). A per-token breach auto-revokes the token and starts an org cooldown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}},"Pagination":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"Schedule":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"color":{"type":"string"},"icon":{"type":["string","null"]},"coverage247":{"type":"boolean"},"flagOverlaps":{"type":"boolean"},"requiredApprovals":{"type":"integer"},"periodMinutes":{"type":"integer"},"orgWideView":{"type":"boolean"}}},"ScheduleDetail":{"allOf":[{"$ref":"#/components/schemas/Schedule"},{"type":"object","properties":{"defaultShiftMinutes":{"type":"integer"},"flagWindow":{},"escalationUserIds":{"type":"array","items":{"type":"string"}},"participants":{"type":"array","items":{"$ref":"#/components/schemas/PersonRef"}},"createdAt":{"type":"string","format":"date-time"}}}]},"ScheduleList":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Schedule"}}}}]},"PersonRef":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":"string"}}},"Person":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":"string"},"title":{"type":["string","null"]},"team":{"type":["string","null"]},"status":{"type":"string"}}},"PersonDetail":{"allOf":[{"$ref":"#/components/schemas/Person"},{"type":"object","properties":{"timezone":{"type":["string","null"]},"orgPermissions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"contacts":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"value":{"type":"string"}}}}}}]},"PersonList":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Person"}}}}]},"Money":{"type":"object","properties":{"currency":{"type":"string"},"amount":{"type":"string"}}},"Report":{"type":"object","properties":{"defaultCurrency":{"type":"string"},"from":{"type":"string"},"to":{"type":"string"},"includeMoney":{"type":"boolean"},"fxAvailable":{"type":"boolean"},"people":{"type":"array","items":{"type":"object"}},"schedules":{"type":"array","items":{"type":"object"}},"bands":{"type":"array","items":{"type":"object"}},"totals":{"type":"object"}}},"OnCall":{"type":"object","properties":{"at":{"type":"string","format":"date-time"},"schedules":{"type":"array","items":{"type":"object","properties":{"scheduleId":{"type":"string"},"scheduleName":{"type":"string"},"escalation":{"type":"array","items":{"type":"object"},"description":"Contact order: index 0 = on call now (kind shift), then fallback (kind escalation)."},"next":{"type":["object","null"]}}}}}}}}}