API overview
A REST API for schedules, people, reporting, who’s on call now, and paging the on-call person.
On this page
OnCallRadar exposes a REST API so you can surface on-call data in status pages, dashboards, and internal tools — and page the on-call person from your own automation. Every response is JSON and scoped to the token’s organization. The data endpoints never change anything; the only action endpoint is starting an escalation. A machine-readable OpenAPI 3.1 spec describes every endpoint and schema.
Creating a token
Only the owner can manage API tokens, in Organization settings. When you create one, the secret is shown once — copy it immediately; only a hash is stored. Revoke a token any time and it stops working instantly.
Authenticating
Send the token as a bearer header on every request:
curl https://oncallradar.com/api/v1/on-call \
-H "Authorization: Bearer ocr_your_token_here"A missing or invalid token returns 401; a revoked token stops resolving immediately.
Common conventions
List endpoints accept two query params and wrap their results in a pagination envelope:
limit— integer, 1–200, default 50.offset— integer ≥ 0, default 0.- The response is
{ "data": [...], "total", "limit", "offset" }, wheretotalis the unfiltered count for the page’s collection.
All timestamps are ISO-8601 in UTC. IDs are opaque strings. Money amounts are strings (to avoid float rounding) paired with an ISO-4217 currency code. A 404 is returned for an id that doesn’t exist in your organization.
List schedules
GET /api/v1/schedules — params: limit, offset. Returns the org’s schedules ordered by position.
{
"data": [
{
"id": "sch_1a2b3c",
"name": "Platform — Primary",
"description": null,
"color": "#7c5cff",
"icon": null,
"coverage247": true, // gaps flagged when true
"flagOverlaps": true, // double-coverage flagged when true
"requiredApprovals": 1,
"periodMinutes": 60, // org min slot
"orgWideView": false
}
],
"total": 4,
"limit": 50,
"offset": 0
}Get a schedule
GET /api/v1/schedules/{id} — no query params. Returns one schedule with its participants (people on the on-call list) and escalation chain.
{
"id": "sch_1a2b3c",
"name": "Platform — Primary",
"description": null,
"color": "#7c5cff",
"icon": null,
"coverage247": true,
"flagOverlaps": true,
"flagWindow": null,
"requiredApprovals": 1,
"periodMinutes": 60,
"defaultShiftMinutes": 480,
"orgWideView": false,
"escalationUserIds": ["usr_grace"],
"createdAt": "2026-05-02T12:00:00.000Z",
"participants": [
{ "id": "usr_ada", "name": "Ada Lovelace", "email": "[email protected]" }
]
}List people
GET /api/v1/people — params: limit, offset. Returns active people (deleted accounts are excluded), ordered by name.
{
"data": [
{
"id": "usr_ada",
"name": "Ada Lovelace",
"email": "[email protected]",
"title": "SRE",
"team": "Platform",
"status": "ACTIVE" // ACTIVE | INVITED | PENDING | DECLINED
}
],
"total": 12,
"limit": 50,
"offset": 0
}Get a person
GET /api/v1/people/{id} — no query params. Adds the person’s timezone, organization permissions, and ordered contact methods.
{
"id": "usr_ada",
"name": "Ada Lovelace",
"email": "[email protected]",
"title": "SRE",
"team": "Platform",
"status": "ACTIVE",
"timezone": "Europe/Sofia",
"orgPermissions": ["MANAGE_SCHEDULES"],
"createdAt": "2026-05-02T12:00:00.000Z",
"contacts": [
{ "kind": "phone", "value": "+1-555-0100" },
{ "kind": "whatsapp", "value": "+1-555-0100" }
]
}Reports
GET /api/v1/reports — hours and cost aggregated per person, pay band, and schedule, with cross-currency totals converted to the org default currency. Parameters (all optional):
from,to— ISO-8601 timestamps bounding the range. Defaults to the current month;tois exclusive. Invalid dates return 400.scope—all(default),past(clamp to elapsed time), orfuture(remaining time).scheduleId— restrict to a single schedule.userId— restrict to a single person.
Cost is grouped by currency and never summed across currencies; converted/convertedCost give an approximate total in defaultCurrency using daily FX rates. fxAvailable is false when rates couldn’t be fetched (amounts are still returned per currency).
{
"defaultCurrency": "USD",
"from": "2026-06-01T00:00:00.000Z",
"to": "2026-07-01T00:00:00.000Z",
"includeMoney": true,
"fxAvailable": true,
"people": [
{
"userId": "usr_ada", "name": "Ada Lovelace", "email": "[email protected]",
"deleted": false,
"normalHours": 120, "premiumHours": 24, "totalHours": 144,
"rate": 60, "currency": "EUR", "rateMissing": false,
"cost": "8640.00", // in the person's own currency
"convertedCost": "9331.20" // approx, in defaultCurrency
}
],
"schedules": [
{ "scheduleId": "sch_1a2b3c", "name": "Platform — Primary", "totalHours": 144,
"costs": [{ "currency": "EUR", "amount": "8640.00" }],
"converted": "9331.20",
"lines": [{ "currency": "EUR", "amount": "8640.00", "rate": 1.08, "converted": "9331.20" }] }
],
"bands": [
{ "multiplier": 1, "hours": 120, "costs": [...], "converted": "...", "lines": [...] },
{ "multiplier": 2, "hours": 24, "costs": [...], "converted": "...", "lines": [...] }
],
"totals": {
"totalHours": 144,
"costs": [{ "currency": "EUR", "amount": "8640.00" }],
"converted": "9331.20",
"lines": [{ "currency": "EUR", "amount": "8640.00", "rate": 1.08, "converted": "9331.20" }]
}
}On call now
GET /api/v1/on-call — no params. A point-in-time snapshot (at) of every schedule. escalation is the full contact order: index 0 is whoever is on call now (kind: "shift"), followed by the standing fallback chain (kind: "escalation"). These indexes line up 1:1 with the index param of starting an escalation. next is the upcoming shift (or null). Each entry carries the person’s ordered contacts.
{
"at": "2026-06-13T09:00:00.000Z",
"schedules": [
{
"scheduleId": "sch_1a2b3c",
"scheduleName": "Platform — Primary",
"escalation": [
{ // index 0 — on call now
"shiftId": "shf_77",
"userId": "usr_ada", "userName": "Ada Lovelace", "userEmail": "[email protected]",
"userTitle": "SRE", "userTeam": "Platform",
"startAt": "2026-06-13T08:00:00.000Z",
"endAt": "2026-06-13T16:00:00.000Z",
"priority": 0,
"source": "ROTATION", // ROTATION | MANUAL
"kind": "shift", // shift | escalation
"contacts": [{ "kind": "phone", "value": "+1-555-0100" }]
},
{ // index 1 — fallback contact
"userId": "usr_grace", "userName": "Grace Hopper",
"kind": "escalation", "escalationSource": "schedule",
"contacts": [{ "kind": "phone", "value": "+1-555-0199" }]
}
],
"next": {
"userId": "usr_alan", "userName": "Alan Turing",
"startAt": "2026-06-13T16:00:00.000Z",
"endAt": "2026-06-14T00:00:00.000Z",
"kind": "shift"
}
}
]
}Calling configuration
GET /api/v1/voice/config — no params. Reports whether your organization can place calls or send SMS, so a client can check before starting an escalation. configured is true only when both provider credentials and a caller ID are present.
{
"provider": "twilio",
"configured": true,
"voice": { "enabled": true },
"sms": { "enabled": false }
}Start an escalation
POST /api/v1/escalations — the one action endpoint: it calls or texts a schedule’s on-call order until someone is reached. It returns immediately with an id; poll the status endpoint for the result. Calling must be configured (see above). JSON body:
scheduleId(required) — the schedule whose on-call order to page.index— page only the person at this 0-based on-call position (0= on call now,1= next in the chain, …), with no follow-ups to anyone else. Omitindexto cascade the whole chain.channel—voice,sms, orauto(default). `auto` prefers calling whenever voice is configured (even if SMS is too) and uses SMS only for an SMS-only org.retries— voice attempts per person before moving on (1–10). Omitted → your organization’s default (set under Calls & SMS, 3 unless changed). SMS is never retried, so this has no effect on an SMS escalation.message— override the schedule/org default message for this escalation.
How each channel cascades:
- Voice (and
autowhen calling is configured) — call each person, retrying up toretriestimes (defaulting to your org setting), and move to the next only when they don’t answer; stop as soon as someone acknowledges. - SMS (and
autoon an SMS-only org) — text the first person we can reach and stop there. SMS isn’t retried and can’t be acknowledged, so a successful send is the goal; we move to the next person only if a message can’t be sent (e.g. an unverified number).
Returns 400 if calling isn’t configured or the requested channel isn’t enabled, and 404 if the schedule isn’t in your organization.
curl -X POST https://oncallradar.com/api/v1/escalations \
-H "Authorization: Bearer ocr_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "scheduleId": "sch_1a2b3c", "channel": "auto" }'
{
"escalation": {
"id": "esc_9f8e7d",
"status": "running",
"channel": "auto",
"scheduleId": "sch_1a2b3c"
}
}Escalation status
GET /api/v1/escalations/{id} — the progress and result of an escalation: its status, who acknowledged (if anyone), and every call/SMS attempt in order. Poll until status is no longer running.
status—running,acknowledged(someone answered a call),notified(an SMS was sent — SMS can’t be acknowledged),exhausted(the calls went unanswered), orfailed(nothing could be sent).acknowledgedBy—{ userId, name, at }once someone acks a call, otherwisenull(alwaysnullfor SMS).attempts[]— each{ channel, to, name, userId, attempt, status, acknowledged, ackMethod, acknowledgedAt, at }, oldest first.
{
"id": "esc_9f8e7d",
"scheduleId": "sch_1a2b3c",
"channel": "auto",
"status": "acknowledged",
"acknowledgedBy": {
"userId": "usr_ada", "name": "Ada Lovelace",
"at": "2026-06-13T09:01:12.000Z"
},
"createdAt": "2026-06-13T09:00:00.000Z",
"updatedAt": "2026-06-13T09:01:12.000Z",
"attempts": [
{
"channel": "voice", "to": "+1-555-0100",
"name": "Ada Lovelace", "userId": "usr_ada",
"attempt": 1, "status": "completed", "acknowledged": true,
"ackMethod": "dtmf", "acknowledgedAt": "2026-06-13T09:01:12.000Z",
"at": "2026-06-13T09:00:05.000Z"
}
]
}Rate limits
Requests are limited per token (60/min) and per IP (80/min). Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; a 429 includes Retry-After. Back off when you see it.
Exceeding the per-token limit auto-revokes that token and starts an org cooldown (15 min, doubling on repeat violations) during which no new token can be issued. The API can page people but cannot edit rosters — all schedule changes still go through the in-app approval workflow.