Developers

The AlertRoster API

An event becomes an incident, the incident pages whoever is on call, and it keeps paging until somebody acknowledges. Every one of those steps has an endpoint.

The console sends real requests against this server with whatever credential you give it, and holds that credential for the session only — never in storage. The spec is the same document: load it into Postman, Insomnia or a client generator and you have the same thing on your own machine.

This page is generated from the running application's own route table. Version 0.1.129 describes 81 endpoints — if it is served here, it is documented here.

On this page

Getting started

Every request goes to your account's own hostname over HTTPS. Bodies are JSON, identifiers are UUIDs, and timestamps are ISO 8601 in UTC to the second.

There are two lanes and they take different credentials, because they have different callers.

/api/v1

Authenticated as a responder holding an access token. This is what the iOS and desktop clients are written against — a person acting on their own behalf.

/api/v2

Authenticated as a system holding an integration key or an account token — a monitor or a ticket system acting on behalf of a ticket.

The two are deliberately not reachable with each other's credentials. A key carries its lane, and a key for one lane is refused on the other.

Integrations

Three of these are published and open source. Two of them deliberately do not use the API on this page, and the badge on each card says which — worth reading before you go looking for a key you do not need.

n8n

Uses this API

Community node — drive AlertRoster from a workflow

Install n8n-nodes-alertroster from Settings → Community Nodes. Two nodes appear: AlertRoster, which raises and drives incidents, reads and edits on-call schedules, acts on handoffs and overrides and runs check-ins; and AlertRoster Trigger, which starts a workflow when an incident changes state. It speaks the API on this page. An integration key (ark_async_… for the event firehose, ark_sync_… for incidents) or an account token (art_…) covers the system-facing resources; a responder credential covers the rest. The node checks the key's prefix against the operation before calling, so a key on the wrong lane is caught in n8n rather than as a 401 here.
https://github.com/CloudBedrock/n8n-nodes-alertroster →

Home Assistant

Does not use this API

HACS integration — ring the panel, and know if nobody answered

Pairs Home Assistant with an AlertRoster receiver station on your own network — the desktop or Raspberry Pi application that sounds an alert, drives relays and wall displays, and waits for somebody to acknowledge. An automation raises an alert; Home Assistant reacts when it is acknowledged, resolved, or expires with nobody answering. It does not use this API. The integration talks to the station over your LAN and nothing in that path touches the internet. If the station holds a cloud key then it escalates off-site, and Home Assistant only ever sees the result. So there is no integration key to mint for it. Pre-release: add it as a HACS custom repository. It needs a receiver station on the same network with Accept sources from the LAN turned on.
https://github.com/CloudBedrock/alertroster-hacs →

Asterisk

Does not use this API

ARI escalation engine — phone a roster until somebody answers

A self-hosted voice escalation engine built on the Asterisk REST Interface. POST /alert and it phones a roster in turn, plays the message, and keeps escalating until somebody presses a digit to acknowledge — optionally blasting the same alert out a paging or PA endpoint at the same time. Each leg is a live ARI-controlled call, with per-call ring and acknowledgement timeouts driving the escalation. It does not use this API either, and it does not depend on AlertRoster at all. It has its own POST /alert, and anything that can send JSON can drive it — a monitor, a cron job, a webhook, a shell script. It is the open-source telephony connector behind AlertRoster, published on its own terms. Asterisk 16+ with ARI enabled; tested on 22.
https://github.com/CloudBedrock/alertroster-ari →

The iOS, Android, desktop, on-premises agent and receiver-firmware clients are not open source. They are written against the contracts this page documents — if you are building something similar, the endpoint reference below is the same thing they were built from.

Authentication

Every credential travels in the same header. Which one you hold decides what you can reach.

Authorization: Bearer YOUR_TOKEN
Opaque access token
A responder's own access token, from POST /api/v1/auth/login or POST /api/v1/auth/refresh. Opaque and database-backed, never a JWT, so revoking one stops it on the next request — docs/AUTHENTICATION.md §6. It lives 15 minutes. Most of /api/v1 accepts one up to five minutes past expiry, because a phone waking to a page must be able to acknowledge before its refresh completes; the routes that change who gets paged do not, and say so in their description.
art_…
An account-level API token, the alternative to a sync integration key on Lane B when one caller acts for several sources. Returned once at creation; only its digest is stored. docs/LANE_B_API.md §3.
ark_async_…
An async integration key, held by a monitor firing events at Lane A (POST /api/v2/enqueue). Scoped to one source. An ark_sync_… key is refused here, and there is no account-level API token on this lane at all: a token names its source per request, and a firehose credential that could name any source would let every monitor raise events as every other one. docs/LANE_A_API.md §3.
ark_sync_…
A sync integration key, held by a ticket system of record calling Lane B (/api/v2/incidents). Scoped to one source. An ark_async_… key is refused here. docs/LANE_B_API.md §3.

Access tokens are opaque and database-backed rather than signed, so revoking one stops it on the very next request. They live fifteen minutes; refresh with POST /api/v1/auth/refresh, which rotates the pair.

Conventions

Transitions are POSTs that name themselves

You acknowledge an incident by posting to /acknowledge, not by patching a status field. A caller that names the transition it wants cannot ask for one the transition table would refuse, and the server never has to guess what a status change was meant to mean.

Reads are cheap, and lists carry what a screen needs

List endpoints return the same object shape their singular counterparts do, with enough on it to render a screen without a second round trip. A client that has the list has the screen.

A stale token still stops a page

Most of /api/v1 accepts an access token up to five minutes past expiry, because a phone that woke to an alert has to be able to acknowledge before its refresh completes. The endpoints that change who gets paged do not, and each one says so.

Errors

Failures answer a machine-readable code rather than prose, so a client can branch on the code and show its own words.

{
  "error": "not_found"
}

A 422 adds a per-field details object naming what was wrong:

{
  "error": "invalid_request",
  "details": {
    "deadline_at": [
      "must be in the future"
    ]
  }
}
Status What it means
200 Read, or a write whose result is the object.
201 Created. The new object is in the body.
202 Accepted and durably written. Not yet acted on.
204 Done, and there is nothing to say.
401 Missing, malformed, or expired credential.
403 A credential that is valid but not permitted here.
404 No such object — or one belonging to somebody else.
409 Well formed, but the world is not in a state that admits it.
422 Well formed, but a field is wrong. `details` says which.
429 Rate limited. A refusal of this attempt, never of the next.

What is not on this page

Two WebSocket surfaces, because OpenAPI has no way to describe them: the incident channel at /socket, which carries live incident state and presence to the clients, and the agent channel at /agent, which carries the command set for the on-premises local agent. Both are specified in the repository alongside the HTTP contracts.

Endpoint reference

Generated from the route table, so it lists exactly what the server serves.

Authentication

Signing in and staying signed in. See docs/AUTHENTICATION.md in the repository.

POST /api/v1/auth/login

Spend a magic link or a login code

No credential required.

Exchanges the emailed token — or the code and email pair — for an access token and a refresh token. The link is single-use, and GETting it in a browser does not spend it, so a mail scanner cannot burn it before the recipient clicks.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "Xy7…"
}'

Response 200

{
  "user": {
    "id": "9a2b…",
    "name": "Ada Walker",
    "email": "ada@example.com",
    "role": "responder"
  },
  "refresh_token": "…",
  "account": {
    "id": "14a1…",
    "name": "Cloud Bedrock",
    "slug": "cloud-bedrock"
  },
  "expires_in": 900,
  "access_token": "…"
}
DELETE /api/v1/auth/logout

Revoke this token family

Requires a responder access token.

Authenticated with the access token whose family it revokes, and a stale one is accepted: revocation is never something to refuse. A client logs out by deregistering its device first and revoking second, so a phone whose token lapsed does not end up signed out and still a push target.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/auth/logout" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/auth/refresh

Exchange a refresh token for a new pair

No credential required.

Rotates the family: the refresh token sent is spent and a new pair comes back. Re-using a spent refresh token revokes the whole family, because a replayed one means either a bug or a stolen credential and there is no way to tell them apart — docs/AUTHENTICATION.md §6.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{
  "refresh_token": "…"
}'

Response 200

{
  "refresh_token": "…",
  "expires_in": 900,
  "access_token": "…"
}

Devices

Registering a phone as a push target. See docs/DEVICE_API.md in the repository.

POST /api/v1/devices

Register — or re-register — this device for push

Requires a responder access token.

One POST both creates and updates. A client re-registers on every launch and cannot know which it is doing, so there is deliberately no separate update route. platform is ios, android or desktop; the first two carry a push token — an APNs device token or an FCM registration token, validated separately and never interchangeable — and only ios carries a push_environment. docs/DEVICE_API.md §3. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/devices" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "app_version": "1.4.0",
  "platform": "ios",
  "push_token": "…",
  "device_identifier": "1A2B-3C4D"
}'

Response 200

{
  "device": {
    "id": "c81d…",
    "platform": "ios",
    "device_identifier": "1A2B-3C4D"
  }
}
DELETE /api/v1/devices/{device_identifier}

Deregister this device

Requires a responder access token.

Removes a delivery target and nothing else — it can never mint anything, which is why it sits with the stale-grace routes rather than behind a fresh token. A client logs out by deregistering then revoking (docs/DEVICE_API.md §8), and a phone whose token lapsed must not get a 401 on the first step and a 204 on the second.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/devices/{device_identifier}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Incidents

The page itself: read it, acknowledge, resolve, reassign. See docs/INCIDENT_API.md in the repository.

GET /api/v1/incidents

List incidents

Requires a responder access token.

What a responder's list screen is built from. docs/INCIDENT_API.md §2 states the latency budget this route is held to, because the list is what a phone opens to while a page is still sounding.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/incidents" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "incidents": [
    {
      "id": "4c1f…",
      "priority": "emergency",
      "status": "triggered",
      "title": "api-gateway 5xx above threshold",
      "emergency": true,
      "urgency": "high",
      "triggered_at": "2026-09-06T17:02:11Z"
    }
  ]
}
GET /api/v1/incidents/{id}

Read one incident

Requires a responder access token.

The same object the list renders — docs/INCIDENT_API.md §3 defines it once.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/incidents/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "triggered",
    "emergency": true
  }
}
POST /api/v1/incidents/{id}/acknowledge

Acknowledge — stop the paging

Requires a responder access token.

The tap that stops a roster being paged, so it is on the hot path: an access token up to five minutes past expiry is accepted here, because a responder whose token lapsed while the phone slept still has to be able to answer. A POST naming the transition rather than a PATCH carrying a status — docs/INCIDENT_API.md §1.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/acknowledge" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "acknowledged"
  }
}
POST /api/v1/incidents/{id}/reassign

Hand the incident to somebody else

Requires a responder access token.

Moves the page to user_id, who is notified. The incident stays open — reassigning is not acknowledging on somebody else's behalf.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/reassign" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "user_id": "9a2b…"
}'

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "triggered",
    "assigned_user_id": "9a2b…"
  }
}
GET /api/v1/incidents/{id}/report

The keyed link to this incident's responder report

Requires a responder access token.

{"report": null} when nothing has been shared. view_count and last_viewed_at answer the question a roster member actually has while a search is running — has anybody opened this — without reading the timeline. docs/INCIDENT_API.md §14. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/incidents/{id}/report" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "report": {
    "id": "b41c…",
    "url": "https://alertroster.com/r/…",
    "expires_at": "2026-09-14T18:14:02Z",
    "view_count": 3
  }
}
POST /api/v1/incidents/{id}/report

Share a missing-person report with responders outside the roster

Requires a responder access token.

Mints the capability URL a roster member hands to a search team, and returns the one that already exists rather than a second — there is one live link per incident, so sharing twice cannot invalidate a QR code already printed and carried. 201 on a fresh mint, 200 on the link that was already there. The response body is a credential. Anyone holding that URL can read the subject's photograph, description and captured positions with no login, which is why this route demands a current access token and why the link is revocable, expiring and logged. docs/INCIDENT_API.md §14. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/report" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 201

{
  "report": {
    "id": "b41c…",
    "url": "https://alertroster.com/r/…",
    "expires_at": "2026-09-14T18:14:02Z",
    "view_count": 0
  }
}
DELETE /api/v1/incidents/{id}/report

Withdraw the report link

Requires a responder access token.

The link stops answering on the next request. 204 whether or not one was live — somebody saying "take that page down" needs the page down, not a 404 about a link that was already gone. docs/INCIDENT_API.md §14. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/incidents/{id}/report" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/incidents/{id}/resolve

Resolve

Requires a responder access token.

Closes the incident. Resolving an already-resolved incident is not an error.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/resolve" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "resolved"
  }
}
POST /api/v1/incidents/{id}/search/beacon

Light the subject's torch and sound their phone

Requires a responder access token.

The loudest thing this product can do to somebody's phone, so it is gated on the subject's beacon consent and, on a duress activation, on an explicit confirm_duress: true — only a literal true, so a client that sent the string "false" cannot light the phone of somebody under coercion. mode is steady, strobe or off, and names the state rather than carrying a boolean: "off" is a third thing rather than the absence of the other two. docs/INCIDENT_API.md §13. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/search/beacon" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "strobe",
  "confirm_duress": true
}'

Response 202

{
  "command": {
    "id": "77ab…",
    "status": "queued",
    "kind": "beacon_strobe"
  }
}
POST /api/v1/incidents/{id}/search/locate

Ask the subject's device for a fresh fix

Requires a responder access token.

Queues a command the phone answers on its next wake. The device is never told why — docs/CHECKIN_API.md §15 — and this route answers with the command rather than a position, because the fix has not happened yet. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/search/locate" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 202

{
  "command": {
    "id": "77aa…",
    "status": "queued",
    "kind": "locate"
  }
}
POST /api/v1/incidents/{id}/silence

Silence this incident's noise on this device

Requires a responder access token.

Stops the sound without acknowledging: the incident is still open and still somebody's problem. The two are separate on purpose — silencing a phone in a meeting must not tell a roster that the page has been answered.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/silence" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "triggered"
  }
}

Check-ins

A heartbeat for a person — arm, extend, satisfy, cancel. See docs/CHECKIN_API.md in the repository.

GET /api/v1/checkins

List your own check-ins

Requires a responder access token.

Oldest first, and only ever your own. Each carries its details — what you said you were wearing and where you said you were going — so the screen renders from one read. docs/CHECKIN_API.md §3.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/checkins" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "checkins": [
    {
      "id": "5f0f…",
      "label": "Evening walk",
      "state": "armed",
      "kind": "timer",
      "details": null,
      "require_code": false,
      "deadline_at": "2026-09-06T18:15:00Z"
    }
  ]
}
POST /api/v1/checkins

Create a check-in

Requires a responder access token.

A timer is the ad-hoc "expect me by 6:15"; a daily recurs at local_time in time_zone. Each check-in mints its own source, and escalation_schedule_id on that source decides who a miss pages. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Evening walk",
  "kind": "timer",
  "reminder_lead_seconds": 900
}'

Response 201

{
  "checkin": {
    "id": "5f0f…",
    "label": "Evening walk",
    "state": "idle"
  }
}
GET /api/v1/checkins/beacon

Whether you have opted in to the beacon

Requires a responder access token.

A client asks before it offers the capability at all.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/checkins/beacon" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "beacon_opt_in": false
}
PUT /api/v1/checkins/beacon

Grant or withdraw beacon consent

Requires a responder access token.

Withdrawing extinguishes a torch that is burning right now, which is why the revoke is ungated on the same terms as location's. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/checkins/beacon" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "opt_in": true
}'

Response 200

{
  "beacon_opt_in": true
}
GET /api/v1/checkins/code

Whether a check-in code and a duress code are set

Requires a responder access token.

The only thing any surface ever says about either code. Neither is rendered anywhere, and docs/CHECKIN_API.md §11 states that as a contract rather than a current fact.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/checkins/code" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "code_set": true,
  "duress_code_set": false
}
PUT /api/v1/checkins/code

Set or replace your check-in code

Requires a responder access token.

Optionally sets a duress code in the same call — both travel through one changeset because the rule that matters most about them, that they differ, can only be checked with both plaintexts in hand. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/checkins/code" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "142857",
  "duress_code": "271828"
}'

Response 200

{
  "code_set": true,
  "duress_code_set": true
}
DELETE /api/v1/checkins/code

Clear your check-in code

Requires a responder access token.

Any check-in that required it stops requiring it. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/checkins/code" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "code_set": false,
  "duress_code_set": false
}
GET /api/v1/checkins/commands

Commands waiting for this device

Requires a responder access token.

What the subject's phone is told, and the only shape it is ever told it in: a command id, a kind, and a deadline. No incident, no responder, no reason — a duress activation may be being watched over the subject's shoulder. docs/CHECKIN_API.md §15.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/checkins/commands" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "commands": [
    {
      "kind": "locate",
      "expires_at": "2026-09-06T17:12:11Z",
      "command_id": "77aa…"
    }
  ]
}
POST /api/v1/checkins/commands/{id}/ack

Report what happened to a command

Requires a responder access token.

A POST naming the answer rather than a PATCH on the command: a device reports what happened, it does not edit a row. outcome is one of ok, unsupported, denied or unavailable. Accepted on a stale token, because a phone woken by a silent push has whatever token it woke with, and a refresh in the middle of a search is a round trip nobody can afford. The answer is the same three-key device payload every command surface uses — no incident, no responder, no reason.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/commands/{id}/ack" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "outcome": "ok"
}'

Response 200

{
  "command": {
    "kind": "locate",
    "expires_at": "2026-09-06T17:12:11Z",
    "command_id": "77aa…"
  }
}
DELETE /api/v1/checkins/duress_code

Clear your duress code

Requires a responder access token.

Leaves the ordinary check-in code alone. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/checkins/duress_code" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "code_set": true,
  "duress_code_set": false
}
GET /api/v1/checkins/location

Whether you have opted in to location

Requires a responder access token.

A client asks this before every capture, to know whether to attach a position at all — which is why it is on the hot half with the transitions rather than behind a fresh token.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/checkins/location" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "location_opt_in": false
}
PUT /api/v1/checkins/location

Grant or withdraw location consent

Requires a responder access token.

Withdrawing also purges what was already captured, and is deliberately not gated on the account feature flag: a flag may stop somebody collecting more, it may not trap them into keeping what is there. docs/POSITIONING_AND_CLAIMS.md §3. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/checkins/location" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "opt_in": true
}'

Response 200

{
  "location_opt_in": true
}
GET /api/v1/checkins/profile

Your reusable search description

Requires a responder access token.

The half that does not change per walk: the vehicle, and the dated photographs. null when you have typed nothing. docs/CHECKIN_API.md §16.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/checkins/profile" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "profile": {
    "vehicle": {
      "year": 2019,
      "colour": "grey",
      "make": "Subaru",
      "model": "Outback",
      "plate": "8XYZ123"
    },
    "vehicle_photo": null,
    "photo": null
  }
}
PUT /api/v1/checkins/profile

Set your reusable search description

Requires a responder access token.

Refused with 409 consent_missing without :details consent — a refusal, deliberately unlike the transitions: somebody filling in a form has asked a question and is owed an answer. The plate is encrypted at rest and nothing can query on it. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/checkins/profile" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "vehicle_make": "Subaru",
  "vehicle_model": "Outback",
  "vehicle_plate": "8XYZ123"
}'

Response 200

{
  "profile": {
    "vehicle": {
      "make": "Subaru",
      "model": "Outback"
    }
  }
}
POST /api/v1/checkins/profile/photo

Mint a presigned upload for a photograph

Requires a responder access token.

The first of three calls: the server chooses the key and signs a PUT, the device uploads straight to the bucket, and a third call makes the server look at what actually arrived. Nothing renders until then, because a presigned PUT is a licence to write arbitrary bytes. docs/CHECKIN_API.md §17. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/profile/photo" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "byte_size": 184320,
  "kind": "person",
  "content_type": "image/jpeg",
  "taken_at": "2026-09-01"
}'

Response 201

{
  "upload_url": "https://…",
  "photo": {
    "id": "cc33…",
    "kind": "person",
    "url": null,
    "taken_at": "2026-09-01",
    "age_days": 5
  },
  "upload_expires_at": "2026-09-06T17:12:11Z"
}
DELETE /api/v1/checkins/profile/photo/{id}

Delete a photograph

Requires a responder access token.

Removes the object and the row. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/checkins/profile/photo/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/checkins/profile/photo/{id}/confirm

Confirm an uploaded photograph

Requires a responder access token.

The server HEADs the object and believes what it finds rather than what the upload claimed. Only after this does the photograph get a URL — and it is handed out with its date and never without it, because a six-month-old photograph presented as current sends people looking for somebody in a coat they no longer own. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/profile/photo/{id}/confirm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "photo": {
    "id": "cc33…",
    "kind": "person",
    "url": "https://…",
    "taken_at": "2026-09-01",
    "age_days": 5
  }
}
PATCH /api/v1/checkins/{id}

Reconfigure a check-in

Requires a responder access token.

Label, reminder lead, a daily's zone or time, and the escalation schedule. kind is immutable and the armed state moves only through the transitions. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v1/checkins/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Evening walk with the dog"
}'

Response 200

{
  "checkin": {
    "id": "5f0f…",
    "label": "Evening walk with the dog"
  }
}
DELETE /api/v1/checkins/{id}

Delete a check-in

Requires a responder access token.

Its source is left alone — it may carry incident history. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/checkins/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/checkins/{id}/arm

Arm — start the clock

Requires a responder access token.

Sets the deadline the miss is measured against. Optionally carries a location and a details object in one call, so a phone starting a walk makes one request rather than three. Neither can fail the transition.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/{id}/arm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "details": {
    "destination_text": "the ridge",
    "wearing": "red jacket"
  },
  "deadline_at": "2026-09-06T18:15:00Z"
}'

Response 200

{
  "checkin": {
    "id": "5f0f…",
    "state": "armed",
    "deadline_at": "2026-09-06T18:15:00Z"
  }
}
POST /api/v1/checkins/{id}/cancel

Stand down without satisfying

Requires a responder access token.

The "never mind" for a timer armed by mistake, and the off switch for a daily. Idempotent, and gated on the code where extend deliberately is not: cancelling takes the safety net down for good.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/{id}/cancel" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "checkin": {
    "id": "5f0f…",
    "state": "idle"
  }
}
PUT /api/v1/checkins/{id}/details

Set what you are wearing and where you are going, on one check-in

Requires a responder access token.

A sentence and a pin are both answers, and they are independent: send either, both, or neither. Read it back on the check-in object rather than here — docs/CHECKIN_API.md §16. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/checkins/{id}/details" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "destination_latitude": 47.7423,
  "destination_longitude": -121.9857,
  "destination_text": "the ridge above Duvall",
  "wearing": "red jacket, blue rucksack"
}'

Response 200

{
  "details": {
    "origin": null,
    "destination": {
      "text": "the ridge above Duvall",
      "latitude": 47.7423,
      "longitude": -121.9857
    },
    "wearing": "red jacket, blue rucksack",
    "captured_at": null
  }
}
POST /api/v1/checkins/{id}/extend

Push the deadline out

Requires a responder access token.

expected_deadline_at, when sent, must match the row — which is what makes a lock-screen "+15 minutes" safe against a double tap and against extending a deadline a daily roll has already replaced.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/{id}/extend" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "expected_deadline_at": "2026-09-06T18:15:00Z",
  "by_seconds": 900
}'

Response 200

{
  "checkin": {
    "id": "5f0f…",
    "state": "armed",
    "deadline_at": "2026-09-06T18:30:00Z"
  }
}
POST /api/v1/checkins/{id}/require_code

Require a code to satisfy or cancel this check-in

Requires a responder access token.

Refused with 409 no_code_set if the caller has no check-in code yet: a flag that could be turned on without one would lock somebody out of their own check-in. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/{id}/require_code" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "require_code": true
}'

Response 200

{
  "checkin": {
    "id": "5f0f…",
    "require_code": true
  }
}
POST /api/v1/checkins/{id}/satisfy

The one tap: "I'm here"

Requires a responder access token.

A timer goes idle; a daily rolls to its next occurrence. Send code when the check-in requires one. Nothing in the response distinguishes a duress satisfy from an ordinary one, and that is the design — docs/CHECKIN_API.md §12.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/{id}/satisfy" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "142857"
}'

Response 200

{
  "checkin": {
    "id": "5f0f…",
    "state": "idle",
    "deadline_at": null
  }
}

Schedules and on-call

On-call schedules, who is on call, handoffs and overrides. See docs/SCHEDULE_API.md in the repository.

GET /api/v1/handoffs

Handoffs waiting on you

Requires a responder access token.

What somebody has offered you, and what you have offered and not heard back on. Narrow with status. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/handoffs" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "handoffs": [
    {
      "id": "99ef…",
      "status": "pending",
      "to_user_id": "9a2b…"
    }
  ]
}
POST /api/v1/handoffs/{id}/accept

Take the pager

Requires a responder access token.

From this moment the escalation path resolves to you for the handoff's window. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/handoffs/{id}/accept" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "handoff": {
    "id": "99ef…",
    "status": "accepted"
  }
}
POST /api/v1/handoffs/{id}/cancel

Withdraw an offer you made

Requires a responder access token.

Only the offerer can cancel, and only while the offer is still open. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/handoffs/{id}/cancel" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "handoff": {
    "id": "99ef…",
    "status": "cancelled"
  }
}
POST /api/v1/handoffs/{id}/decline

Refuse the pager

Requires a responder access token.

The offer closes and the rota is unchanged. The offerer is told. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/handoffs/{id}/decline" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "handoff": {
    "id": "99ef…",
    "status": "declined"
  }
}
PATCH /api/v1/layers/{id}

Reconfigure a layer

Requires a responder access token.

Its name, its rotation, or where it sits in the stack. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v1/layers/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Weekdays (early)"
}'

Response 200

{
  "layer": {
    "id": "aa11…",
    "name": "Weekdays (early)"
  }
}
DELETE /api/v1/layers/{id}

Delete a layer

Requires a responder access token.

The layers beneath it apply where it used to. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/layers/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
PUT /api/v1/layers/{id}/members

Set who is in a layer's rotation, and in what order

Requires a responder access token.

The whole list, not a delta: order is the rotation, and a partial write would make "who is next" depend on the order the writes arrived in. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/layers/{id}/members" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "user_ids": [
    "9a2b…",
    "9a2c…"
  ]
}'

Response 200

{
  "layer": {
    "id": "aa11…",
    "member_ids": [
      "9a2b…",
      "9a2c…"
    ]
  }
}
PUT /api/v1/layers/{id}/restrictions

Set when a layer applies

Requires a responder access token.

The windows within a week the layer covers — "weekdays, nine to six". The whole set, for members' reason. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PUT "https://your-account.alertroster.com/api/v1/layers/{id}/restrictions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "restrictions": [
    {
      "until": "18:00:00",
      "from": "09:00:00",
      "day": "monday"
    }
  ]
}'

Response 200

{
  "layer": {
    "id": "aa11…"
  }
}
DELETE /api/v1/overrides/{id}

Remove an override

Requires a responder access token.

The rota underneath it applies again from the moment this returns. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/overrides/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v1/schedules

List schedules

Requires a responder access token.

Every on-call schedule in the account. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/schedules" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "schedules": [
    {
      "id": "7714…",
      "name": "Primary on-call",
      "time_zone": "America/Denver"
    }
  ]
}
POST /api/v1/schedules

Create a schedule

Requires a responder access token.

A schedule carries the time zone its rotations are computed in. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/schedules" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Primary on-call",
  "time_zone": "America/Denver"
}'

Response 201

{
  "schedule": {
    "id": "7714…",
    "name": "Primary on-call"
  }
}
GET /api/v1/schedules/{id}

Read one schedule

Requires a responder access token.

The schedule with its layers. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/schedules/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "schedule": {
    "id": "7714…",
    "name": "Primary on-call",
    "layers": []
  }
}
PATCH /api/v1/schedules/{id}

Rename a schedule or move its time zone

Requires a responder access token.

Changing the zone re-computes who is on call from the next boundary. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v1/schedules/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Primary on-call (US)"
}'

Response 200

{
  "schedule": {
    "id": "7714…",
    "name": "Primary on-call (US)"
  }
}
DELETE /api/v1/schedules/{id}

Delete a schedule

Requires a responder access token.

Sources pointing at it fall back to the loud default: a miss pages every responder in the account. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/schedules/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/schedules/{id}/handoffs

Offer the pager to somebody

Requires a responder access token.

An offer, not a fact: the recipient accepts or declines, and the pager does not move until they do. docs/SCHEDULE_API.md §6. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/schedules/{id}/handoffs" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "ends_at": "2026-09-07T00:00:00Z",
  "starts_at": "2026-09-06T18:00:00Z",
  "to_user_id": "9a2b…"
}'

Response 201

{
  "handoff": {
    "id": "99ef…",
    "status": "pending"
  }
}
GET /api/v1/schedules/{id}/on_call

Who is on call right now

Requires a responder access token.

Resolves the layers, the overrides and the handoffs into a single answer. This is the question the escalation path asks, exposed so a client can ask it too — docs/SCHEDULE_API.md §4. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/schedules/{id}/on_call" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "at": "2026-09-06T16:00:00Z",
  "on_call": {
    "via": {
      "position": 1,
      "type": "layer"
    },
    "user_id": "9a2b…"
  }
}
POST /api/v1/schedules/{id}/overrides

Override the rota for a window

Requires a responder access token.

"Bea is covering Ada from six until midnight." An override beats the layers underneath it for its window and nothing else. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/schedules/{id}/overrides" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "user_id": "9a2b…",
  "ends_at": "2026-09-07T00:00:00Z",
  "starts_at": "2026-09-06T18:00:00Z"
}'

Response 201

{
  "override": {
    "id": "88cd…",
    "user_id": "9a2b…",
    "ends_at": "2026-09-07T00:00:00Z",
    "starts_at": "2026-09-06T18:00:00Z"
  }
}
GET /api/v1/schedules/{id}/roster

The look-ahead: who is on call, and when, over a window

Requires a responder access token.

The same resolution as on_call, computed forward over a window so a client can draw a rota rather than ask repeatedly. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/schedules/{id}/roster" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "from": "2026-09-06T16:00:00Z",
  "to": "2026-09-13T16:00:00Z",
  "segments": [
    {
      "ends_at": "2026-09-07T16:00:00Z",
      "starts_at": "2026-09-06T16:00:00Z",
      "on_call": {
        "via": {
          "position": 1,
          "type": "layer"
        },
        "user_id": "9a2b…"
      }
    }
  ]
}
GET /api/v1/schedules/{schedule_id}/layers

List a schedule's layers

Requires a responder access token.

Layers stack: the last one that covers an instant wins. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/schedules/{schedule_id}/layers" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "layers": [
    {
      "id": "aa11…",
      "name": "Weekdays",
      "position": 1
    }
  ]
}
POST /api/v1/schedules/{schedule_id}/layers

Add a layer

Requires a responder access token.

A rotation over a set of members, with a restriction on when it applies. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/schedules/{schedule_id}/layers" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Weekdays",
  "rotation": "weekly",
  "handoff_at": "09:00:00"
}'

Response 201

{
  "layer": {
    "id": "aa11…",
    "name": "Weekdays"
  }
}

Receivers

Hardware receivers: claim, enrol, and the certificate that follows. See docs/RECEIVER_API.md in the repository.

GET /api/v1/receivers

List the account's receivers

Requires a responder access token.

Hardware that lights, sounds, or relays a page. Their state and their enrolment. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/receivers" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "receivers": [
    {
      "id": "bb22…",
      "name": "Bay 3 beacon",
      "scope": "account",
      "last_seen_at": "2026-09-06T17:40:00Z",
      "model": "m5stack-basic",
      "enrolled_at": "2026-09-06T17:02:11Z",
      "hardware_id": "24:6f:28:aa:bb:cc",
      "certificate_expires_at": "2027-09-06T17:02:11Z"
    }
  ]
}
POST /api/v1/receivers

Create a receiver and mint its claim

Requires a responder access token.

The claim is the one-time credential the device spends at POST /receivers/enroll, along with the account_id it belongs to. It is returned once — docs/RECEIVER_API.md §3. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/receivers" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Bay 3 beacon",
  "model": "m5stack-basic",
  "hardware_id": "24:6f:28:aa:bb:cc"
}'

Response 201

{
  "receiver": {
    "id": "bb22…",
    "name": "Bay 3 beacon",
    "model": "m5stack-basic"
  },
  "account_id": "14a1…",
  "user_id": "9a2b…",
  "claim_expires_at": "2026-09-07T17:02:11Z",
  "claim_token": "…",
  "receiver_host": "receivers.alertroster.com"
}
GET /api/v1/receivers/catalogue

The receiver models this account can enrol

Requires a responder access token.

What a provisioning screen offers, and what firmware each model expects. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/receivers/catalogue" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "receivers": [
    {
      "id": "m5stack-basic",
      "name": "M5Stack Basic",
      "status": "tested",
      "firmware": null,
      "chip": "ESP32"
    }
  ]
}
POST /api/v1/receivers/enroll

Spend a claim for a client certificate

No credential required.

Unauthenticated by construction: the claim in the body is the credential, which is why this route carries no Authorization header and is metered per client IP like the other unauthenticated sign-in routes. The device sends a CSR and gets back a certificate it then uses for mTLS. docs/RECEIVER_API.md §4.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/receivers/enroll" \
  -H "Content-Type: application/json" \
  -d '{
  "account_id": "14a1…",
  "claim_token": "…",
  "csr": "-----BEGIN CERTIFICATE REQUEST-----\n…"
}'

Response 200

{
  "certificate": "-----BEGIN CERTIFICATE-----\n…",
  "receiver": {
    "id": "bb22…",
    "name": "Bay 3 beacon",
    "enrolled_at": "2026-09-06T17:02:11Z"
  },
  "certificate_expires_at": "2027-09-06T17:02:11Z",
  "ca_certificate": "-----BEGIN CERTIFICATE-----\n…"
}
DELETE /api/v1/receivers/{id}

Delete a receiver

Requires a responder access token.

Revokes its certificate. The device stops being able to connect immediately. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/receivers/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Responders

The responders in an account. See docs/AUTHENTICATION.md in the repository.

GET /api/v1/users

List the responders in the account

Requires a responder access token.

Who can be paged, and who a schedule or a reassignment can name.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/users" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "users": [
    {
      "id": "9a2b…",
      "name": "Ada Walker",
      "email": "ada@example.com"
    }
  ]
}
DELETE /api/v1/users/me

Delete your own account

Requires a responder access token.

The account holder deletes themselves; nobody deletes anybody else here. What survives is what an incident history has to keep, and docs/AUTHENTICATION.md says which. Requires a current access token. The five-minute grace the rest of /api/v1 allows does not apply here: this route changes who gets paged, and nobody needs to do that in the five minutes before a refresh completes. A lapsed token answers 401; refresh and retry.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/users/me" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Lane A — the event firehose

The async firehose a monitor fires events at. See docs/LANE_A_API.md in the repository.

POST /api/v2/enqueue

Fire an event at the firehose

Requires an async integration key (ark_async_…).

The async lane: the caller is a monitor with nowhere to put an answer, so what comes back is a receipt for a durable write and never the state of an incident. Everything that decides what the event means — dedup, whether it opens an incident, who it pages — happens in a job the same transaction enqueued. event_action is required and never defaulted: a caller that meant to resolve and mistyped the field would otherwise raise a fresh incident, which is the loudest possible way to be wrong about a payload. It is trigger, acknowledge or resolve, and payload is required on a trigger. docs/LANE_A_API.md §5.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/enqueue" \
  -H "Authorization: Bearer ark_async_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "payload": {
    "source": "db-primary.iad",
    "severity": "critical",
    "summary": "Primary database is not responding",
    "custom_details": {
      "region": "iad",
      "checks_failed": 3
    }
  },
  "dedup_key": "db-primary-down",
  "event_action": "trigger"
}'

Response 202

{
  "status": "accepted",
  "dedup_key": "db-primary-down",
  "alert_id": "dd44…"
}

Lane B — the incidents API

The synchronous surface a ticket system of record calls. See docs/LANE_B_API.md in the repository.

POST /api/v2/incidents

Open an incident, synchronously

Requires a sync integration key (ark_sync_…) or an account API token (art_…).

The sync lane: the caller is a ticket system of record and gets the incident back, because it has somewhere to put it. Idempotent on dedup_key — the same key answers with the same incident rather than opening a second one.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/incidents" \
  -H "Authorization: Bearer ark_sync_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Payment gateway timeouts",
  "dedup_key": "INC-4471",
  "urgency": "high"
}'

Response 201

{
  "incident": {
    "id": "4c1f…",
    "status": "triggered",
    "dedup_key": "INC-4471"
  }
}
GET /api/v2/incidents/{id}

Read an incident

Requires a sync integration key (ark_sync_…) or an account API token (art_…).

There is deliberately no list route on this lane: a ticket system holds the ids it created, and an account-wide list would make one source's key a bulk reader of every incident in the tenant.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/incidents/{id}" \
  -H "Authorization: Bearer ark_sync_YOUR_KEY"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "triggered"
  }
}
PUT /api/v2/incidents/{id}

Revise what an incident says

Requires a sync integration key (ark_sync_…) or an account API token (art_…).

Title and urgency — what the incident says, never what it is. The status moves only through the transitions below.

Request

curl -X PUT "https://your-account.alertroster.com/api/v2/incidents/{id}" \
  -H "Authorization: Bearer ark_sync_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Payment gateway timeouts (EU only)",
  "urgency": "low"
}'

Response 200

{
  "incident": {
    "id": "4c1f…",
    "title": "Payment gateway timeouts (EU only)"
  }
}
POST /api/v2/incidents/{id}/acknowledge

Acknowledge on behalf of the ticket

Requires a sync integration key (ark_sync_…) or an account API token (art_…).

Stops the paging. A POST naming the transition, for the reason /api/v1's is.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/incidents/{id}/acknowledge" \
  -H "Authorization: Bearer ark_sync_YOUR_KEY"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "acknowledged"
  }
}
POST /api/v2/incidents/{id}/reassign

Reassign on behalf of the ticket

Requires a sync integration key (ark_sync_…) or an account API token (art_…).

Moves the page to user_id. The incident stays open.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/incidents/{id}/reassign" \
  -H "Authorization: Bearer ark_sync_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "user_id": "9a2b…"
}'

Response 200

{
  "incident": {
    "id": "4c1f…",
    "assigned_user_id": "9a2b…"
  }
}
POST /api/v2/incidents/{id}/resolve

Resolve on behalf of the ticket

Requires a sync integration key (ark_sync_…) or an account API token (art_…).

Closes the incident when the system of record closes the ticket.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/incidents/{id}/resolve" \
  -H "Authorization: Bearer ark_sync_YOUR_KEY"

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "resolved"
  }
}