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 1.3.53 describes 236 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. It is 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.
t=<unix>,v1=<hex>: HMAC-SHA256 over "<t>.<raw body>" with the secret X-AlertRoster-Key names: platform (the shared platform secret) or partner (the partner's own callback secret). Five minutes either way. The same scheme AlertRoster signs its own callbacks with. docs/REFERRAL_LEADS.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/companions

Sign in the caller's Apple Watch

Requires a responder access token.

The signed-in phone mints its Apple Watch a token family of its own and hands the pair over, so the Watch reaches the server with no phone in range and nobody types credentials on a wrist. kind is apple_watch, and refresh_token is the phone's own live refresh token: minting needs both halves of the phone's pair. The Watch's family is the phone's child, revoked with it, and one per phone. companion_id is how the phone revokes it (docs/AUTHENTICATION.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/auth/companions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "apple_watch",
  "refresh_token": "…"
}'

Response 201

{
  "refresh_token": "…",
  "expires_in": 900,
  "access_token": "…",
  "companion_id": "0d6c…"
}
DELETE /api/v1/auth/companions/{id}

Revoke the caller's Apple Watch

Requires a responder access token.

Revokes the companion family the phone minted, and ends any socket the Watch holds on it. Always 204, idempotent, and the same for an id that is somebody else's or nobody's; a stale token is accepted, because revocation is never refused (docs/AUTHENTICATION.md §6).

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/auth/companions/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
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. See 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, desktop or watchos (an Apple Watch reached directly, §9e); all but desktop carry a push token (an APNs device token or an FCM registration token, validated separately and never interchangeable), and ios and watchos carry 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/crew

Who is on shift, and where

Requires a responder access token.

The people on shift that the caller may see: anybody with an armed check-in, or an open incident raised from one, each with the newest position of the current period and its fix_at, so a client draws it with its age. Never a position from a check-in that has ended, never a person whose check-in could not page the caller (the incident's own rule for an open incident), and position is null when there is none to show. docs/CHECKIN_API.md §22. 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/crew" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "crew": [
    {
      "name": "Wren Worker",
      "position": {
        "latitude": 47.61,
        "longitude": -122.33,
        "accuracy_m": 12.0,
        "captured_at": "2026-10-05T19:58:11.204Z",
        "fix_at": "2026-10-05T19:58:10Z"
      },
      "user_id": "9a2b…",
      "checkin_id": "c41e…",
      "incident_id": null,
      "deadline_at": "2026-10-05T22:00:00Z",
      "on_shift": "checkin"
    }
  ]
}
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",
      "description": "5xx rate 12% over 5 min, threshold 2%",
      "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. See 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}/approve

Approve: answer a dev-approval incident yes

Requires a responder access token.

Records the caller's approval on an open dev-approval incident and resolves it; the decision is then written back to the tracker issue the agent was working. Offered only when available_actions lists approve. 409 when the incident closed or somebody decided first, 422 when it is not a dev-approval incident. See docs/INCIDENT_API.md §24.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/approve" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Go ahead, keep the migration reversible."
}'

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "resolved",
    "decision": {
      "note": "Go ahead, keep the migration reversible.",
      "decision": "approve",
      "decided_at": "2026-10-07T14:03:00Z",
      "decided_by_user_id": "9a2b…"
    }
  }
}
POST /api/v1/incidents/{id}/choose

Choose: answer a dev-approval question with one of its options

Requires a responder access token.

Records which of a question's numbered options the caller chose and resolves the incident; the agent's hook types that number, and the choice is written back to the tracker issue. Offered in Approve's place when available_actions lists choose. option is an integer and must be one of the incident's options[].n: 422 invalid_option otherwise, 422 not_offered on an incident that is not a question. 409 when the incident closed or somebody decided first. See docs/INCIDENT_API.md §24.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/choose" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "option": 2,
  "note": "Keep the old column until the apps ship."
}'

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "resolved",
    "options": [
      {
        "label": "Drop the column now",
        "n": 1
      },
      {
        "label": "Keep it",
        "n": 2
      }
    ],
    "decision": {
      "option": 2,
      "note": "Keep the old column until the apps ship.",
      "decision": "choose",
      "decided_at": "2026-10-07T14:03:00Z",
      "decided_by_user_id": "9a2b…"
    }
  }
}
GET /api/v1/incidents/{id}/context

Conditions at the destination a missing person named

Requires a responder access token.

What the sources say now about the destination this incident's subject stated then: the weather that closed in, the fire they walked toward. The destination is the copy frozen when the miss raised; the conditions are live. The same object GET /checkins/:id/briefing returns, because the clients merge the two into one model. 404 no_destination on any incident with no frozen located destination, which is most of them. A source that is down is reported in sources rather than failing the request, so check complete before telling a responder that nothing is happening. docs/CHECKIN_API.md §21. 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}/context" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "briefing": {
    "complete": true,
    "sources": [],
    "items": [],
    "lines": [
      "Severe Thunderstorm Warning · Missoula, MT · National Weather Service · issued 12 min ago"
    ],
    "destination": {
      "text": "the ridge above Duvall",
      "latitude": 47.7423,
      "longitude": -121.9857
    },
    "stated_at": "2026-09-15T17:02:11Z",
    "radius_km": 50,
    "assembled_at": "2026-09-15T21:14:02Z"
  }
}
GET /api/v1/incidents/{id}/last_known_positions

Where each responder's inReach last reported from

Requires a responder access token.

The last-known position of every inReach carried by somebody on this incident (the same people POST /incidents/:id/locate may ask), and how old it is. fix_at is the unit's own time for the fix, not when we received it; show it beside the coordinates, and compute the age against as_of rather than the device clock. A unit that has never reported a fix has position: null, and a refresh from Garmin is requested in the background. Open incidents only: a resolved one answers 409 incident_closed. See docs/INCIDENT_API.md §18. 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}/last_known_positions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "as_of": "2026-09-18T19:30:00Z",
  "last_known_positions": [
    {
      "position": {
        "latitude": 43.80787,
        "longitude": -70.16367,
        "fix_at": "2026-09-18T19:02:11Z"
      },
      "last_seen_at": "2026-09-18T19:02:40Z",
      "user_id": "9a2b…",
      "imei": "300434030000000",
      "device_id": "c31f…",
      "low_battery": false
    }
  ]
}
POST /api/v1/incidents/{id}/locate

Ask a responder's inReach where it is

Requires a responder access token.

Asks the inReach of a responder on this incident (the assignee, anybody its current rung is waiting on, or anybody paged for it) for its position over Iridium. Costs the customer's airtime and counts against the monthly cap. Name the unit by user_id or device_id, not both. The answer arrives on the incident's timeline as locate_answered when the unit next checks its mail, or as locate_failed if the request did not go. One unanswered locate per unit at a time: a second answers 409 locate_outstanding with the first. See docs/INCIDENT_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/incidents/{id}/locate" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "user_id": "9a2b…"
}'

Response 202

{
  "locate": {
    "id": "5d0e…",
    "status": "queued",
    "requested_at": "2026-09-18T19:02:11Z",
    "user_id": "9a2b…",
    "imei": "300434030000000",
    "device_id": "c31f…"
  }
}
GET /api/v1/incidents/{id}/media/{media_id}

Read a photo or voice clip sent from the field

Requires a responder access token.

The bytes behind a field_media timeline entry: a photo or voice clip a responder sent from a Plus inReach, attached to this incident. media_id is the entry's data.media_id. The body is the file itself, with its own content-type (image/avif and audio/ogg are what Garmin sends); any type outside the known image and audio types is served as an application/octet-stream download instead. A voice clip's transcription is on the entry, not here. 404 media_not_found for an id that is not on this incident; 410 media_gone when the stored copy can no longer be read. See docs/INCIDENT_API.md §19.

Request

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

Read the incident's thread

Requires a responder access token.

Messages oldest first, after after_seq (exclusive), at most limit (default 200, max 500), with has_more. Each carries its author, the 160-character rendering a unit receives, where the sender was (location, always present: attached, correlated, a place, or none with the reason) and the current delivery state per recipient and channel. A client that reconnects asks for everything after the last seq it holds and misses nothing. docs/INCIDENT_API.md §23.

Request

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

Response 200

{
  "messages": [
    {
      "id": "8e2a…",
      "location": {
        "source": "correlated",
        "latitude": 47.75,
        "longitude": -121.85,
        "accuracy_m": 12.0,
        "fix_at": "2026-10-06T14:31:50Z",
        "place_id": null,
        "no_location_reason": null,
        "gap_s": 40
      },
      "seq": 12,
      "author": {
        "user_id": "9a2b…",
        "device_id": null
      },
      "body": "Heading up the north ridge now.",
      "kind": "text",
      "rendering": [
        "S.Searcher: Heading up the north ridge now."
      ],
      "created_at": "2026-10-06T14:32:30Z",
      "deliveries": [
        {
          "at": "…",
          "state": "sent",
          "channel": "push",
          "recipient_user_id": "5f61…"
        }
      ],
      "arrived_via": "api"
    }
  ],
  "has_more": false,
  "incident_id": "4c1f…"
}
POST /api/v1/incidents/{id}/messages

Post a message to the incident's thread

Requires a responder access token.

client_message_id (1 to 255 characters, required) makes it idempotent: a resend returns the stored message with 200, a new one is 201. kind is text (default), location (with a location object) or location_request; a location answering a request names it in answers_message_id. Attach the sender's fix as location, or say why there is none with no_location_reason (not_shared from a desk skips correlation). Every participant is told: over the socket when they hold one, by an ordinary push otherwise. docs/INCIDENT_API.md §23.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/messages" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "location": {
    "latitude": 47.75,
    "longitude": -121.85,
    "accuracy_m": 12.0,
    "fix_at": "2026-10-06T14:31:50Z"
  },
  "body": "Heading up the north ridge now.",
  "kind": "text",
  "client_message_id": "a1b2c3d4-5e6f-…"
}'

Response 201

{
  "message": {
    "id": "8e2a…",
    "seq": 12,
    "kind": "text"
  }
}
GET /api/v1/incidents/{id}/participants

Who the incident's thread is for

Requires a responder access token.

Each participant's user_id and why they are on it: assignee, paged, subject (the check-in's owner), author or added. Never the duress subject. docs/INCIDENT_API.md §23.5.

Request

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

Response 200

{
  "incident_id": "4c1f…",
  "participants": [
    {
      "user_id": "9a2b…",
      "roles": [
        "paged",
        "author"
      ]
    }
  ]
}
POST /api/v1/incidents/{id}/participants

Add somebody to the incident's thread

Requires a responder access token.

user_id of any member of the account who can see the incident: 201 when added, 200 when already on it, 422 unknown_user otherwise. Recorded on the timeline as participant_added. docs/INCIDENT_API.md §23.5.

Request

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

Response 201

{
  "incident_id": "4c1f…",
  "participants": [
    {
      "user_id": "5f61…",
      "roles": [
        "added"
      ]
    }
  ]
}
GET /api/v1/incidents/{id}/plan

The incident's search plan

Requires a responder access token.

The sectors, routes and measurements the search is run in, oldest first, each with its GeoJSON geometry, label, assignee and revision. A plan, never a position: the subject's positions are on /search. Removed items are left out. docs/INCIDENT_API.md §22.

Request

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

Response 200

{
  "items": [
    {
      "id": "7d0e…",
      "label": "Sector B",
      "kind": "sector",
      "revision": 3,
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -121.9,
              47.7
            ],
            [
              -121.8,
              47.7
            ],
            [
              -121.8,
              47.8
            ],
            [
              -121.9,
              47.7
            ]
          ]
        ]
      },
      "assigned_user_id": "9a2b…"
    }
  ]
}
POST /api/v1/incidents/{id}/plan

Add a sector, route or measurement to the plan

Requires a responder access token.

kind is sector (a Polygon or MultiPolygon), route (a LineString or MultiLineString) or measurement (a LineString or Polygon), in WGS 84. An invalid shape is 422 invalid_item; an assignee who is not a member who can see the incident is 422 unknown_user; a closed incident is 409 incident_closed. Pushed to every board as incident_plan_changed. docs/INCIDENT_API.md §22.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/plan" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Sector B",
  "kind": "sector",
  "geometry": {
    "type": "Polygon",
    "coordinates": []
  },
  "assigned_user_id": "9a2b…"
}'

Response 201

{
  "item": {
    "id": "7d0e…",
    "kind": "sector",
    "revision": 1
  }
}
PATCH /api/v1/incidents/{id}/plan/{item_id}

Change a plan item, at the revision you last saw

Requires a responder access token.

revision is required: the item's revision the change was made against. Another is 409 stale_revision with the item as it now stands, so two people editing at once see each other's change instead of overwriting it. Leaving geometry out keeps the shape; the kind is fixed. docs/INCIDENT_API.md §22.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v1/incidents/{id}/plan/{item_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "revision": 3,
  "assigned_user_id": "5f61…"
}'

Response 200

{
  "item": {
    "id": "7d0e…",
    "revision": 4
  }
}
DELETE /api/v1/incidents/{id}/plan/{item_id}

Remove a plan item, at the revision you last saw

Requires a responder access token.

revision as on PATCH, here as a query parameter; a stale one is 409 stale_revision. The item leaves the plan; its history stays on the timeline and the record. docs/INCIDENT_API.md §22.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v1/incidents/{id}/plan/{item_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
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…"
  }
}
POST /api/v1/incidents/{id}/reject

Reject: answer a dev-approval incident no

Requires a responder access token.

Records that the caller asked for changes and resolves the incident; the decision is written back to the tracker issue as changes-requested. Same refusals as approve. See docs/INCIDENT_API.md §24.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/incidents/{id}/reject" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Split the schema change into its own PR."
}'

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "resolved",
    "decision": {
      "note": "Split the schema change into its own PR.",
      "decision": "reject",
      "decided_at": "2026-10-07T14:03:00Z",
      "decided_by_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}/satellite_media

Send a photo from this incident to a responder's inReach

Requires a responder access token.

Sends a photo to the Plus inReach of a responder on this incident, over Iridium: a photo another responder sent from the field ({"field_media": <media_id>}, the media_id of a field_media timeline entry), or the dated photograph of the person or vehicle the search panel shows ({"checkin_photo": <photo_id>}). Name the unit by user_id or device_id, not both; caption is optional and at most 160 characters. Costs the customer's airtime and counts against the monthly cap. The outcome arrives on the timeline as media_sent, or as media_failed with carried: Garmin refuses a unit that is not a Plus radio, and that refusal is how it is reported. 404 media_not_found for a source that is not on this incident; the locate's refusals answer 409 as they do there. See docs/INCIDENT_API.md §20. 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}/satellite_media" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "source": {
    "checkin_photo": "0f6e…"
  },
  "user_id": "9a2b…",
  "caption": "Missing since 14:00, red jacket"
}'

Response 202

{
  "media": {
    "id": "5d0e…",
    "status": "queued",
    "requested_at": "2026-09-21T19:02:11Z",
    "user_id": "9a2b…",
    "imei": "300434030000000",
    "device_id": "c31f…"
  }
}
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 counts, 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"
  }
}
GET /api/v1/incidents/{id}/timeline

Read what has happened to this incident

Requires a responder access token.

Every append-only timeline entry for one incident, oldest first, open or closed: what was triggered, which rung of the ladder fired and who it woke, every page and its outcome, who acknowledged, silenced, reassigned or resolved. This is what an automation posting "here is what happened" on resolve reads, and what an audit reads to answer "who was paged and when" months later. The envelope (seq, at, kind, status, actor_user_id) is the stable part; data carries each kind's own facts as the entry recorded them and may gain keys. Sort and dedup on seq, not on at: seq is assigned under the incident's row lock, and at can tie and can be backdated. Unpaginated by design: see docs/INCIDENT_API.md §16, which documents every kind and its data.

Request

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

Response 200

{
  "entries": [
    {
      "data": {
        "target": "policy_rule",
        "policy_id": "7b0e…",
        "paged_user_ids": [
          "9a2b…",
          "1f3c…"
        ],
        "rule_id": "2d55…",
        "paged_via": [
          "named",
          "schedule:5e21…:layer:1"
        ]
      },
      "status": "triggered",
      "at": "2026-09-17T18:04:11Z",
      "seq": 1,
      "kind": "triggered",
      "actor_user_id": null
    },
    {
      "data": {
        "within_local_grace": false
      },
      "status": "acknowledged",
      "at": "2026-09-17T18:05:02Z",
      "seq": 2,
      "kind": "acknowledged",
      "actor_user_id": "9a2b…"
    }
  ],
  "incident_id": "4c1f…"
}
GET /api/v1/places

The account's places, and its venue UUID

Requires a responder access token.

The rooms and zones a receiver advertises and a phone resolves. A phone caches this so it can name a place it hears with no network at all, which is the point of the design: indoors a GPS fix carries tens of metres of error, and a basement often has none. place_uuid is the other half of the identity: null until the first place exists.

Request

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

Response 200

{
  "places": [
    {
      "id": "7c1e…",
      "major": 2,
      "minor": 17,
      "name": "Bay 3",
      "floor": "1",
      "building": "West"
    }
  ],
  "place_uuid": "3f2a…"
}
POST /api/v1/places

Create a place

Requires a responder access token.

major is the building or floor and minor is the place; the pair is unique per account and is what the iBeacon frame carries. The account's place_uuid is minted on the first place and returned with it. Needs the tier that carries on-site hardware; below it, 402 plan_feature_required. 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/places" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "major": 2,
  "minor": 17,
  "name": "Bay 3",
  "floor": "1",
  "building": "West"
}'

Response 201

{
  "place_uuid": "3f2a…",
  "place": {
    "id": "7c1e…",
    "major": 2,
    "minor": 17,
    "name": "Bay 3"
  }
}
POST /api/v1/places/observations

Record a batch of place sightings

Requires a responder access token.

What a phone heard, in one request. Batched on purpose: a phone that was in a basement flushes a backlog when it surfaces, and that must be one request rather than forty. A triple naming no place yet is stored, not discarded: it is still a record of where somebody was, and POST /places names the backlog when the room finally exists. A uuid that is not this account's venue is another site heard through a wall and is counted, not kept. Rides the docs/CHECKIN_API.md §13 location consent; 403 location_consent_required without it.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/places/observations" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "observations": [
    {
      "major": 2,
      "minor": 17,
      "kind": "enter",
      "uuid": "3f2a…",
      "observed_at": "2026-09-19T14:02:11Z",
      "rssi": -71
    }
  ]
}'

Response 200

{
  "resolved": 9,
  "rejected": 1,
  "stored": 12,
  "unresolved": 3,
  "foreign_venue": 0
}
PATCH /api/v1/places/{id}

Edit a place

Requires a responder access token.

Including major and minor: a mis-keyed number is an ordinary correction, and every receiver assigned to the place is re-pushed its config so a board's frame and the registry cannot drift apart. 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/places/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Bay 3 (loading)",
  "building": "West"
}'

Response 200

{
  "place": {
    "id": "7c1e…",
    "major": 2,
    "minor": 17,
    "name": "Bay 3 (loading)"
  }
}
DELETE /api/v1/places/{id}

Delete a place

Requires a responder access token.

Receivers assigned to it keep their rows and lose the assignment, and are pushed a disabled config so a board stops advertising a room that no longer exists rather than transmitting it until somebody reboots 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/places/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/satellite/sos_reports

Report an SOS the app declared, refused or rehearsed

Requires a responder access token.

Under Bluetooth only there is no Outbound webhook, so an SOS the app declares reaches Garmin Response and nothing reaches AlertRoster. This is how the roster hears of it. A declared report (declared the literal true) is folded into the responder's open check-in-miss incident exactly as a delivered Declare SOS is (promoted to emergency) or opens its own. A declare the radio refused carries refused: already_in_emergency takes that same road, because one is active on that unit and nothing else will say so, while every other reason is recorded as a failed declare and alarmed, never paged. A report with neither is a rehearsal, recorded and never an emergency. Idempotent on report_id (a retry must resend the stored report unchanged), and the unit must be one an administrator registered to the caller. origin is auto (the automatic SOS on a missed check-in) or manual (the responder pressed the panic control); absent reads auto. A manual declare never lands on a check-in-miss incident: it opens its own at emergency. docs/SATELLITE_API.md §14.6.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/satellite/sos_reports" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "origin": "auto",
  "refused": "already_in_emergency",
  "point": {
    "latitude": 47.6062,
    "longitude": -122.3321
  },
  "imei": "300434030000001",
  "battery_percent": 14,
  "checkin_id": "c7d2…",
  "report_id": "9b1c…",
  "refused_detail": "the radio is already in an emergency",
  "declared_at": "2026-09-21T14:02:11Z",
  "declared": false
}'

Response 202

{
  "status": "accepted",
  "outcome": "declared"
}

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,
      "deadline_at": "2026-09-06T18:15:00Z",
      "require_code": false
    }
  ]
}
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, because 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…"
  }
}
PUT /api/v1/checkins/duress_code

Set or change your duress code, and only that

Requires a responder access token.

code proves who you are and is not written. That is the whole reason this route exists: PUT /checkins/code writes what it is given, so a client without this one had to send the check-in code in order to change the second code, and a person who typed the wrong one of their two codes into that field replaced their check-in code with their duress code, which then stopped signalling. 409 no_code_set when there is no check-in code yet; a duress code has nothing to be entered against on its 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 PUT "https://your-account.alertroster.com/api/v1/checkins/duress_code" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "246813",
  "duress_code": "135791"
}'

Response 200

{
  "code_set": true,
  "duress_code_set": true
}
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/fallback

Where to text or call, and whether this phone will be heard

Requires a responder access token.

What an app needs to offer "check in by text" or "check in by phone" when the data connection is gone. The numbers are served rather than built into the client so changing one does not need an App Store release on a safety path, and they are two fields rather than one because they are a single Twilio number by provisioning rather than by contract. null (not omitted) means the channel is unconfigured here. phone_registered: false means an inbound text or call from this responder would be answered with silence, so a client shows no fallback rather than one that quietly does nothing. docs/CHECKIN_API.md §20. auto_sos is whether, and how, the app declares an SOS on the responder's paired inReach when they miss a check-in with no signal: null unless the organization has enabled it and this responder has a registered unit. Cache it: the moment it matters is the moment the app is offline. A client declares only when declare is literally true; on any other value it rehearses. docs/SATELLITE_API.md §14. manual_sos is whether the panic control may declare an SOS on the responder's paired inReach when they press it, and the countdown in which an accidental press is cancelled. Its own switch, beside auto_sos rather than behind it, so a panic control works where automatic SOS is off; the same registered-unit and declare rules. null unless every gate holds. docs/SATELLITE_API.md §14.7. satellite is how the account gets satellite paging (model: byo, hosted or bluetooth_only) and whether the server can page the responder's inReach at all (server_pages); null when the account has never configured satellite. docs/SATELLITE_API.md §22.

Request

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

Response 200

{
  "commands": {
    "cancel": "OFF",
    "extend": "EXTEND",
    "satisfy": "OK"
  },
  "satellite": {
    "model": "byo",
    "server_pages": true
  },
  "sms_number": "+15555550123",
  "voice_number": "+15555550123",
  "auto_sos": {
    "mode": "both",
    "declare": true,
    "reply_to": "acme@sat.alertroster.com",
    "imeis": [
      "300434030000001"
    ],
    "organization": "Acme Utilities",
    "countdown_seconds": 60,
    "message_first_seconds": 600
  },
  "manual_sos": {
    "declare": true,
    "imeis": [
      "300434030000001"
    ],
    "organization": "Acme Utilities",
    "countdown_seconds": 10
  },
  "phone_registered": true
}
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. track carries the sampling cadence for continuous capture, advertised rather than fixed so it moves without a client release.

Request

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

Response 200

{
  "track": {
    "distance_m": 50,
    "final_interval_seconds": 60,
    "final_lead_seconds": 900,
    "interval_seconds": 180
  },
  "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",
      "model": "Outback",
      "make": "Subaru",
      "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. It is 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. Answers 503 encryption_unavailable when a plate is sent and the deployment has configured no encryption key, which is an operator's problem rather than the caller's, like 503 media_unavailable on the photo routes. The unencrypted vehicle fields still save in that state, and so does clearing a plate. 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_plate": "8XYZ123",
  "vehicle_make": "Subaru",
  "vehicle_model": "Outback"
}'

Response 200

{
  "profile": {
    "vehicle": {
      "model": "Outback",
      "make": "Subaru"
    }
  }
}
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. watch_button_ids names the caller's phone-reported panic buttons to watch with this check-in; it needs location consent, and a watch that cannot be tied refuses the whole arm (409 watch_refused, or 422 on the ids). The server ends the watch on satisfy or cancel, and a miss holds it until the miss's incident closes (docs/BUTTON_API.md §5).

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",
  "watch_button_ids": [
    "0b7e…"
  ]
}'

Response 200

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

What the sources say about where you said you were going

Requires a responder access token.

The pre-departure briefing. Every other thing this product does begins after something has gone wrong; this is the one that happens before: what is happening at the destination on this check-in, in the sources' own words, each with whose word it is and how old. It is not advice. Nothing here says a trip is safe, and nothing here is ever a reason a check-in was not armed or an escalation did not run. 404 no_destination when this check-in has no located destination: no :details consent, nothing typed, or a place named in words without :location consent for its coordinates. A source that is down is reported in sources with its own state rather than failing the request, so check complete before showing a reassuring blank. docs/CHECKIN_API.md §21.

Request

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

Response 200

{
  "briefing": {
    "complete": true,
    "sources": [],
    "items": [],
    "lines": [
      "Severe Thunderstorm Warning · Missoula, MT · National Weather Service · issued 12 min ago"
    ],
    "destination": {
      "text": "the ridge above Duvall",
      "latitude": 47.7423,
      "longitude": -121.9857
    },
    "stated_at": "2026-09-15T17:02:11Z",
    "radius_km": 50,
    "assembled_at": "2026-09-15T17:40: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). The coordinates are encrypted at rest, so this answers 503 encryption_unavailable when one is sent and the deployment has configured no encryption key, as PUT /checkins/profile does. The text fields are not encrypted and still save, and a caller without :location consent never sees it: their coordinates are dropped before anything encrypts. 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_text": "the ridge above Duvall",
  "destination_latitude": 47.7423,
  "destination_longitude": -121.9857,
  "wearing": "red jacket, blue rucksack"
}'

Response 200

{
  "details": {
    "origin": null,
    "destination": {
      "text": "the ridge above Duvall",
      "latitude": 47.7423,
      "longitude": -121.9857
    },
    "captured_at": null,
    "wearing": "red jacket, blue rucksack"
  }
}
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}/positions

Stream positions during an armed window or an open incident

Requires a responder access token.

Continuous capture for the length of an armed check-in, batched, and (once a miss has raised an incident) for as long as that incident stays open. Refused unless the caller holds location consent and there is somewhere for the batch to land: an open incident a miss of this check-in raised, or failing that an armed window. Each point's fix_at must fall inside the period it lands in. A malformed element is dropped and counted rather than failing the batch; a replayed batch stores nothing twice. When the incident a miss raised has closed and nothing else is open or armed, the answer is 409 not_armed carrying stop (the incident, its terminal status and resolved_at): the device ends its position session for this check-in. Points captured before resolved_at are still stored; later ones are dropped. It never stops region monitoring or presence. docs/CHECKIN_API.md §13.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/checkins/{id}/positions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "positions": [
    {
      "latitude": 47.6062,
      "longitude": -122.3321,
      "accuracy_m": 12.5,
      "fix_at": "2026-09-08T18:14:02Z"
    }
  ]
}'

Response 200

{
  "dropped": 0,
  "track": {
    "distance_m": 50,
    "final_interval_seconds": 60,
    "final_lead_seconds": 900,
    "interval_seconds": 180
  },
  "stored": 1
}
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
  }
}

Panic buttons

Bluetooth panic buttons: register, arm, report the link, press. See docs/BUTTON_API.md in the repository.

GET /api/v1/buttons

The caller's panic buttons

Requires a responder access token.

Every button the caller has registered, with the server's view of its link: whether it is armed, the last link report and when it arrived, and whether a silence alarm has been raised. The app shows this rather than its own belief, because the server's is the one that pages (docs/BUTTON_API.md §3).

Request

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

Response 200

{
  "buttons": [
    {
      "id": "0b7e…",
      "label": "Left strap",
      "armed": true,
      "link_state": "connected",
      "model": "flic2",
      "silence_window_seconds": 300,
      "alarmed": false
    }
  ]
}
POST /api/v1/buttons

Register a paired button

Requires a responder access token.

Called once the phone has paired the button. Mints the button's own source, which pages the people a missed check-in would page: the escalation_schedule_id given, or every responder in the account. The button starts disarmed and disconnected (docs/BUTTON_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 POST "https://your-account.alertroster.com/api/v1/buttons" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Left strap",
  "serial": "BD12-A3456",
  "model": "flic2"
}'

Response 201

{
  "button": {
    "id": "0b7e…",
    "armed": false,
    "link_state": "disconnected"
  }
}
PATCH /api/v1/buttons/{id}

Rename a button or change its silence window

Requires a responder access token.

label and silence_window_seconds (60 to 3600). A new window applies from the next sweep (docs/BUTTON_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 PATCH "https://your-account.alertroster.com/api/v1/buttons/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Right strap",
  "silence_window_seconds": 600
}'

Response 200

{
  "button": {
    "id": "0b7e…",
    "label": "Right strap",
    "silence_window_seconds": 600
  }
}
DELETE /api/v1/buttons/{id}

Delete a button

Requires a responder access token.

Stops watching it and removes it. Its event record and its source stay (docs/BUTTON_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 DELETE "https://your-account.alertroster.com/api/v1/buttons/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/buttons/{id}/arm

Start watching the button

Requires a responder access token.

From now on, if the link is not reported for longer than the silence window, or is reported disconnected for longer than it, the server raises an incident on the button's source. The clock starts at the arming instant. Idempotent. A phone-reported button is watched only with its owner's armed location check-in, named by checkin_id, and the server ends the watch when that check-in ends; otherwise 409 watch_refused with a stated reason (docs/BUTTON_API.md §5).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/buttons/{id}/arm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "checkin_id": "5f0f…"
}'

Response 200

{
  "button": {
    "id": "0b7e…",
    "watch": {
      "active": true,
      "reason": "checkin",
      "checkin_id": "5f0f…",
      "incident_id": null
    },
    "armed": true,
    "armed_at": "2026-09-30T08:00:00Z"
  }
}
POST /api/v1/buttons/{id}/disarm

Stop watching the button

Requires a responder access token.

End of shift, or the button going in a drawer. Presses still page when disarmed; only the silence watch stops. Idempotent (docs/BUTTON_API.md §5).

Request

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

Response 200

{
  "button": {
    "id": "0b7e…",
    "armed": false
  }
}
POST /api/v1/buttons/{id}/heartbeat

Report the button's link

Requires a responder access token.

Sent about once a minute while armed, and on every link change. link_state is connected or disconnected; battery_percent is optional. The server stamps its own clock on receipt. The response is the server's view, including alarmed (docs/BUTTON_API.md §6).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/buttons/{id}/heartbeat" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "battery_percent": 88,
  "link_state": "connected"
}'

Response 200

{
  "button": {
    "id": "0b7e…",
    "last_heartbeat_at": "2026-09-30T08:05:00Z",
    "armed": true,
    "link_state": "connected",
    "alarmed": false
  }
}
POST /api/v1/buttons/{id}/presses

Report a panic press

Requires a responder access token.

Raises an emergency incident on the button's source, armed or not. client_event_id makes a retried press page once; nothing in the body can make the server refuse a press. gesture: "hold" is a silent duress: the roster is paged and the presser's own devices never are. 202 for a new press and for a repeat (docs/BUTTON_API.md §7). An optional position is put on the incident when the wearer holds location consent; a missing, malformed or unconsented one is dropped and never refuses the press. track in the answer says whether to start the press's position stream (§7.3).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/buttons/{id}/presses" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "position": {
    "latitude": 47.6062,
    "longitude": -122.3321,
    "accuracy_m": 12.5,
    "fix_at": "2026-09-30T08:06:10Z"
  },
  "client_event_id": "6d1f0c52-…",
  "gesture": "press",
  "pressed_at": "2026-09-30T08:06:12Z"
}'

Response 202

{
  "status": "accepted",
  "track": true,
  "event_id": "a41c…"
}
POST /api/v1/buttons/{id}/presses/{client_event_id}/positions

Stream positions for the incident a press raised

Requires a responder access token.

The position stream a panic or duress press started, keyed by the press's own client_event_id, until its incident is resolved. Batches land on the press until the incident is raised and on the incident while it is open. Once it has closed the answer is 409 stopped carrying stop (the press, the terminal status and resolved_at, never the incident); points taken before resolved_at are still stored. Identical for a panic and a duress. Requires location consent (docs/BUTTON_API.md §7.3).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/buttons/{id}/presses/{client_event_id}/positions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "positions": [
    {
      "latitude": 47.6062,
      "longitude": -122.3321,
      "accuracy_m": 12.5,
      "fix_at": "2026-09-30T08:07:02Z"
    }
  ]
}'

Response 200

{
  "dropped": 0,
  "track": {
    "distance_m": 50,
    "final_interval_seconds": 60,
    "final_lead_seconds": 900,
    "interval_seconds": 180
  },
  "stored": 1
}
POST /api/v1/panics

Panic from the app's own control

Requires a responder access token.

A press with no Flic and no inReach: the app's on-screen panic. Same body, answers and retry rules as a button press. gesture: "hold" is a silent duress: the roster is paged as an emergency and the presser's own devices never are. Nothing in the body refuses it (docs/BUTTON_API.md §7.1, §7.2). An optional position and the track answer work as on a button press. origin is phone (the default) or watch for the Apple Watch app; it changes what the roster is told and nothing the presser receives.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/panics" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "position": {
    "reporter": "apple_watch",
    "latitude": 47.6062,
    "longitude": -122.3321,
    "accuracy_m": 12.5,
    "fix_at": "2026-10-01T17:59:58Z"
  },
  "origin": "watch",
  "client_event_id": "6d1f0c52-…",
  "gesture": "hold",
  "pressed_at": "2026-10-01T18:00:00Z"
}'

Response 202

{
  "status": "accepted",
  "track": true,
  "event_id": "a41c…"
}
POST /api/v1/panics/{client_event_id}/positions

Stream positions for the incident a panic raised

Requires a responder access token.

The app control's twin of the button press stream: the same body, the same answers, and the same stop, keyed by the panic's own client_event_id (docs/BUTTON_API.md §7.3).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/panics/{client_event_id}/positions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "positions": [
    {
      "latitude": 47.6062,
      "longitude": -122.3321,
      "accuracy_m": 12.5,
      "fix_at": "2026-10-01T18:01:02Z"
    }
  ]
}'

Response 200

{
  "dropped": 0,
  "track": {
    "distance_m": 50,
    "final_interval_seconds": 60,
    "final_lead_seconds": 900,
    "interval_seconds": 180
  },
  "stored": 1
}

Fall detection

Fall and no-motion detection: opt in, the detector's heartbeat, report, cancel. See docs/DETECTION_API.md in the repository.

POST /api/v1/detections

Report a detected fall, no movement or unusual heart rate

Requires a responder access token.

Opens a window (60 s) on the server and pushes an "Are you OK?" prompt to the caller. Pages nobody by itself: only a window nobody cancels escalates. 201 for a new window, 200 with the existing one for a retry with the same client_event_id, 403 detection_not_enabled without the opt-in (docs/DETECTION_API.md §6). reporter is phone, flic_duo or apple_watch; an Apple Watch fall may carry Apple's own watch_resolution, which is shown to the roster and changes nothing about the window. A heart_rate report may carry what the Watch measured (heart_rate_direction, bpm, sustained_seconds), shown to the roster the same way.

Request

curl -X POST "https://your-account.alertroster.com/api/v1/detections" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "fall",
  "reporter": "apple_watch",
  "confidence": 0.82,
  "client_event_id": "b1c9…",
  "watch_resolution": "unresponsive",
  "detected_at": "2026-09-30T14:02:11Z"
}'

Response 201

{
  "detection": {
    "id": "7f3a…",
    "state": "pending",
    "expires_at": "2026-09-30T14:03:12Z"
  }
}
GET /api/v1/detections/monitor

The caller's fall-detection opt-in and liveness

Requires a responder access token.

Whether the caller has switched detection on, whether the phone's detector is armed, and the server's view of its heartbeat, including alarmed when the server has already told the roster the detector is not being heard (docs/DETECTION_API.md §4).

Request

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

Response 200

{
  "monitor": {
    "enabled": true,
    "armed": true,
    "sensing_state": "active",
    "alarmed": false
  }
}
PUT /api/v1/detections/monitor

Switch fall detection on

Requires a responder access token.

The opt-in: consent to a device inferring a fall, no movement or an unusual heart rate. Off by default; nothing is accepted from a person who has not done this. Optional silence_window_seconds (120 to 3600, default 600). Idempotent (docs/DETECTION_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 PUT "https://your-account.alertroster.com/api/v1/detections/monitor" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "silence_window_seconds": 600
}'

Response 200

{
  "monitor": {
    "enabled": true,
    "armed": false
  }
}
DELETE /api/v1/detections/monitor

Switch fall detection off

Requires a responder access token.

Immediate, and takes a stale token so it never waits on a refresh. Disarms the detector and refuses new reports. A window already open keeps running; cancel it to stop it (docs/DETECTION_API.md §4).

Request

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

Response 200

{
  "monitor": {
    "enabled": false,
    "armed": false
  }
}
POST /api/v1/detections/monitor/arm

Start watching the phone's detector

Requires a responder access token.

From now on, if the phone stops reporting that its detector is running, or keeps reporting that it cannot sense motion, for longer than the window, the server raises an incident. 403 detection_not_enabled without the opt-in (docs/DETECTION_API.md §5).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/detections/monitor/arm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "monitor": {
    "armed": true
  }
}
POST /api/v1/detections/monitor/disarm

Stop watching the phone's detector

Requires a responder access token.

Idempotent (docs/DETECTION_API.md §5).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/detections/monitor/disarm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "monitor": {
    "armed": false
  }
}
POST /api/v1/detections/monitor/heartbeat

Report that the detector is running

Requires a responder access token.

About every two minutes while armed, and at once when motion sensing becomes available or unavailable. sensing_state is active or unavailable; the server stamps its own clock (docs/DETECTION_API.md §5).

Request

curl -X POST "https://your-account.alertroster.com/api/v1/detections/monitor/heartbeat" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "sensing_state": "active"
}'

Response 200

{
  "monitor": {
    "armed": true,
    "sensing_state": "active",
    "alarmed": false
  }
}
GET /api/v1/detections/open

The caller's open detection window

Requires a responder access token.

The pending window, or null. What the app shows when it opens from the prompt, or after a restart (docs/DETECTION_API.md §6).

Request

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

Response 200

{
  "detection": {
    "id": "7f3a…",
    "state": "pending"
  }
}
POST /api/v1/detections/{id}/cancel

I'm OK: cancel a detection window

Requires a responder access token.

200 with the cancelled window. 409 not_pending with the window's real state if it has already ended, including one that escalated: the roster is then already being told, and the app must say so rather than show success (docs/DETECTION_API.md §6).

Request

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

Response 200

{
  "detection": {
    "id": "7f3a…",
    "state": "cancelled"
  }
}

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, such as "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. No tier limits how many there are: the roster and the rotations on it are free on every plan (docs/SCHEDULE_API.md §11). 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 §5. Included from the Lone Worker tier; below it, 403 plan_feature_required (§11). 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. See 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": [
    {
      "on_call": {
        "via": {
          "position": 1,
          "type": "layer"
        },
        "user_id": "9a2b…"
      },
      "ends_at": "2026-09-07T16:00:00Z",
      "starts_at": "2026-09-06T16:00:00Z"
    }
  ]
}
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). A board the account has never seen needs the Lone Worker tier; below it, 403 plan_feature_required. 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…"
}
PATCH /api/v1/receivers/{id}

Assign a receiver to a place, and trim its radio

Requires a responder access token.

The per-unit place settings (docs/RECEIVER_API.md §7a). measured_power is the reference RSSI at one metre and is a property of this board in its enclosure at its mounting, not of the model, which is why correcting a badly-sited unit is this call rather than a reflash. The new config is pushed to the board on the channel at once, and again whenever it rejoins. 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/receivers/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "place_id": "7c1e…",
  "place_advertising": true,
  "measured_power": -65,
  "tx_power_dbm": -12
}'

Response 200

{
  "receiver": {
    "id": "bb22…",
    "place_id": "7c1e…",
    "place_advertising": true,
    "measured_power": -65,
    "tx_power_dbm": -12
  }
}
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"

maps

The map an account draws on: sites, geofences, buildings and floors, hazard areas. See docs/MAP_LAYERS_API.md in the repository.

GET /api/v1/map/features

Every map feature, as GeoJSON

Requires a responder access token.

A GeoJSON FeatureCollection of every feature on every layer, for an account small enough to send whole. Each feature's properties carry name, kind, layer_id, place_id and major over whatever the account stored. At most 5,000 features: past that it is 413 too_many_features with the limit, never a shorter collection, and the client reads tiles or narrows with bbox. A malformed bbox is 400 invalid_bbox. docs/MAP_LAYERS_API.md §4.1.

Request

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

Response 200

{
  "type": "FeatureCollection",
  "features": [
    {
      "id": "e81f…",
      "type": "Feature",
      "properties": {
        "name": "Gate",
        "kind": "geofence",
        "layer_id": "5d0a…"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -97.1,
              32.7
            ],
            [
              -97.0,
              32.7
            ],
            [
              -97.0,
              32.8
            ],
            [
              -97.1,
              32.7
            ]
          ]
        ]
      }
    }
  ]
}
GET /api/v1/map/hazards

What the hazard feeds say around a point, for the map

Requires a responder access token.

Weather warnings, fires, earthquakes and advisories within radius_km of lat,lon (the middle of a map's view, never anybody's position), in the same item and source shape as the check-in briefing. Assembled on demand and never stored. Every source consulted is listed with its state; check complete before showing an empty map as "nothing is happening". An item with no latitude is area-wide: list it with its area, do not place it. Show each item's source.attribution. 400 invalid_point for a missing or out-of-range point; 429 rate_limited with retry-after past 60 reads in 5 minutes per user, since every read spends a budget all tenants share. docs/MAP_LAYERS_API.md §8. 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/map/hazards" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "hazards": {
    "complete": true,
    "sources": [
      {
        "state": "fresh",
        "key": "usgs",
        "answered": true
      }
    ],
    "items": [
      {
        "source": {
          "name": "USGS Earthquake Hazards Program",
          "key": "usgs"
        },
        "kind": "seismic",
        "severity": "moderate",
        "latitude": 47.79,
        "longitude": -121.88,
        "headline": "M 4.1 - 12 km NE of Duvall"
      }
    ],
    "point": {
      "latitude": 47.61,
      "longitude": -122.33
    },
    "radius_km": 50
  }
}
GET /api/v1/map/layers

The account's map layers

Requires a responder access token.

Every layer the account has drawn, by kind then name, with the kinds a layer can be. A layer is a named set of one kind (site, geofence, building, floor, hazard_area) and is what a map styles by. docs/MAP_LAYERS_API.md §3.

Request

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

Response 200

{
  "layers": [
    {
      "id": "5d0a…",
      "name": "Yard",
      "description": null,
      "kind": "geofence"
    }
  ],
  "kinds": [
    "site",
    "geofence",
    "building",
    "floor",
    "hazard_area"
  ]
}
POST /api/v1/map/layers

Create a map layer

Requires a responder access token.

kind is set here and never changes. Admin only. docs/MAP_LAYERS_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/map/layers" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Yard",
  "kind": "geofence"
}'

Response 201

{
  "layer": {
    "id": "5d0a…",
    "name": "Yard",
    "description": null,
    "kind": "geofence"
  }
}
PATCH /api/v1/map/layers/{id}

Rename a map layer

Requires a responder access token.

Changes name or description. kind is ignored. Admin only. 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/map/layers/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "North yard"
}'

Response 200

{
  "layer": {
    "id": "5d0a…",
    "name": "North yard",
    "kind": "geofence"
  }
}
DELETE /api/v1/map/layers/{id}

Delete a map layer

Requires a responder access token.

Deletes the layer and every feature on it. Admin only. 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/map/layers/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v1/map/layers/{id}/features

One layer's features, as GeoJSON

Requires a responder access token.

The FeatureCollection of GET /api/v1/map/features, for one layer, with the same bbox and the same 5,000-feature bound (413 too_many_features). docs/MAP_LAYERS_API.md §4.1.

Request

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

Response 200

{
  "type": "FeatureCollection",
  "features": []
}
POST /api/v1/map/layers/{id}/features

Add a feature to a map layer

Requires a responder access token.

geometry is a GeoJSON geometry in WGS 84: a Point, LineString, Polygon or their Multi forms, in two dimensions. An invalid polygon (self-intersecting, unclosed) is 422. place_id ties it to one room in the places registry; major to every place on a building or floor. Admin only. docs/MAP_LAYERS_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 POST "https://your-account.alertroster.com/api/v1/map/layers/{id}/features" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Gate",
  "properties": {
    "note": "Badge in"
  },
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [
          -97.1,
          32.7
        ],
        [
          -97.0,
          32.7
        ],
        [
          -97.0,
          32.8
        ],
        [
          -97.1,
          32.7
        ]
      ]
    ]
  }
}'

Response 201

{
  "feature": {
    "id": "e81f…",
    "type": "Feature"
  }
}
PATCH /api/v1/map/layers/{id}/features/{feature_id}

Edit a map feature

Requires a responder access token.

Any of the create fields. Leaving geometry out keeps the shape. Admin only. 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/map/layers/{id}/features/{feature_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Main gate"
}'

Response 200

{
  "feature": {
    "id": "e81f…",
    "type": "Feature"
  }
}
DELETE /api/v1/map/layers/{id}/features/{feature_id}

Delete a map feature

Requires a responder access token.

Admin only. 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/map/layers/{id}/features/{feature_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v1/map/tiles/{z}/{x}/{y}

A vector tile of the account's map layers

Requires a responder access token.

A Mapbox Vector Tile (application/vnd.mapbox-vector-tile) at z/x/y in the standard XYZ scheme, cut by PostGIS from the caller's account only. One tile layer per kind. An empty tile is 200 with an empty body. 400 invalid_tile for a coordinate outside the zoom's grid or a zoom past 22. docs/MAP_LAYERS_API.md §5.

Request

curl -X GET "https://your-account.alertroster.com/api/v1/map/tiles/{z}/{x}/{y}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v1/map/token

A short-lived ArcGIS token for the basemap

Requires a responder access token.

An ArcGIS access token for basemaps and location services, so no installed client holds a long-lived Esri credential. Minted from the account's own ArcGIS when it has connected one (source: "account"), from AlertRoster's otherwise (source: "alertroster"), and from AlertRoster's when the account's fails, with account_error saying why. Cached server-side; refresh on expires_at. 503 with retry-after when no source can mint: keep drawing with the token held and say so on the map. The same 503 with reason allowance_exceeded and a retry-after of 900 when the account used its month's map allowance on AlertRoster's credential and no incident is open on it; an open incident is always served. license_string is the Native Maps SDK licence string a Swift, Kotlin or Qt client passes to setLicense, for the SDK major named by sdk_major, or null when none is configured for it; the 503 carries it too. docs/MAP_LAYERS_API.md §7.

Request

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

Response 200

{
  "source": "alertroster",
  "token": "3NKHt6i2urmWtqOuugvr9…",
  "expires_at": "2026-10-04T14:00:00Z",
  "basemap_style_url": null,
  "basemap_session": {
    "token": "AAPTxy8B…",
    "expires_at": "2026-10-06T02:00:00Z"
  },
  "license_string": "runtimelite,1000,rud…",
  "account_error": null
}

cases

Missing-animal cases: open one, attach and review sightings, add dated photographs. See docs/CASES_API.md in the repository.

GET /api/v1/cases

The account's missing-animal cases

Requires a responder access token.

Newest first. status filters to one of open, sighted, recovered, closed. Coordinates are full precision: this is the holding tenant reading its own cases. docs/CASES_API.md §3.

Request

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

Response 200

{
  "cases": [
    {
      "id": "9b1e…",
      "name": "Biscuit",
      "status": "open",
      "last_seen_at": "2026-10-05T18:20:00Z",
      "behaviour_profile": "flight_risk_dog",
      "species": "dog",
      "last_seen": {
        "latitude": 32.7555,
        "longitude": -97.3308
      },
      "public_url": "https://alertroster.com/c/…"
    }
  ]
}
POST /api/v1/cases

Open a case

Requires a responder access token.

species, last_seen_at, latitude and longitude are required. The response carries manage_token once and only once: it is stored as a digest, so hand it to the owner now, or rotate it later. public_url is stable for the life of the case, so a printed flyer never goes dead. docs/CASES_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 POST "https://your-account.alertroster.com/api/v1/cases" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Biscuit",
  "last_seen_at": "2026-10-05T18:20:00Z",
  "latitude": 32.7555,
  "longitude": -97.3308,
  "behaviour_profile": "flight_risk_dog",
  "contact_name": "Sam Ortiz",
  "contact_phone": "+18175550142",
  "species": "dog"
}'

Response 201

{
  "case": {
    "id": "9b1e…",
    "status": "open"
  },
  "manage_token": "…"
}
GET /api/v1/cases/{id}

One case, with its sightings and photographs

Requires a responder access token.

Sightings in observed_at order, every status, and track: the confirmed ones as a trail with distance, direction and speed between them, and the legs that contradict each other. Photographs are the confirmed ones, each with a URL that lives minutes and its taken_at and age_days; a client that draws the image without its age is non-conformant. docs/CASES_API.md §3.

Request

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

Response 200

{
  "case": {
    "id": "9b1e…",
    "status": "sighted"
  },
  "track": {
    "points": [
      {
        "sighting_id": "40aa…",
        "observed_at": "2026-10-05T19:05:00Z"
      }
    ],
    "legs": [],
    "conflicts": []
  },
  "media": [
    {
      "id": "c3f2…",
      "url": "https://…",
      "taken_at": "2026-09-30T12:00:00Z",
      "age_days": 5
    }
  ],
  "sightings": [
    {
      "id": "40aa…",
      "status": "confirmed",
      "observed_at": "2026-10-05T19:05:00Z"
    }
  ]
}
PATCH /api/v1/cases/{id}

Change a case, or finish it

Requires a responder access token.

Any detail, and the last-seen point as a latitude and longitude pair. status may only become recovered or closed, and then outcome (reunited, found_deceased, withdrawn) is required. A finished case refuses every change. docs/CASES_API.md §5. 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/cases/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "recovered",
  "outcome": "reunited"
}'

Response 200

{
  "case": {
    "id": "9b1e…",
    "status": "recovered",
    "outcome": "reunited"
  }
}
POST /api/v1/cases/{id}/manage_token

Replace the owner's manage token

Requires a responder access token.

The old token stops working at once and the new one is returned once. There is no way to read a manage token back. docs/CASES_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 POST "https://your-account.alertroster.com/api/v1/cases/{id}/manage_token" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "case": {
    "id": "9b1e…"
  },
  "manage_token": "…"
}
POST /api/v1/cases/{id}/media

Ask for a photograph upload URL

Requires a responder access token.

taken_at, content_type (image/jpeg, image/png, image/heic) and byte_size are required; sighting_id attaches it to a sighting on this case. PUT the bytes to upload.url, then confirm. 503 media_unavailable when no bucket is configured. docs/CASES_API.md §7. 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/cases/{id}/media" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "byte_size": 240000,
  "content_type": "image/jpeg",
  "taken_at": "2026-09-30T12:00:00Z"
}'

Response 201

{
  "upload": {
    "method": "PUT",
    "url": "https://…",
    "expires_at": "2026-10-05T19:10:00Z"
  },
  "media": {
    "id": "c3f2…",
    "sighting_id": null,
    "taken_at": "2026-09-30T12:00:00Z"
  }
}
DELETE /api/v1/cases/{id}/media/{media_id}

Delete a photograph

Requires a responder access token.

The row goes now and the object follows. docs/CASES_API.md §7. 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/cases/{id}/media/{media_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v1/cases/{id}/media/{media_id}/confirm

Confirm an uploaded photograph

Requires a responder access token.

The server reads the object and records what is actually there. 422 object_missing when the upload has not landed (try again); too_large or unusable delete the row. docs/CASES_API.md §7. 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/cases/{id}/media/{media_id}/confirm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "media": {
    "id": "c3f2…",
    "url": "https://…",
    "age_days": 5
  }
}
POST /api/v1/cases/{id}/sightings

Add a sighting

Requires a responder access token.

observed_at, latitude, longitude and confidence (certain, probable, possible) are required. observed_at is when it was seen, never defaulted to now. Arrives unreviewed. 409 case_finished on a recovered or closed case. docs/CASES_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/cases/{id}/sightings" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "latitude": 32.7581,
  "longitude": -97.3274,
  "accuracy_m": 25,
  "confidence": "probable",
  "notes": "Running north on Main, no collar visible",
  "observed_at": "2026-10-05T19:05:00Z"
}'

Response 201

{
  "sighting": {
    "id": "40aa…",
    "status": "unreviewed",
    "source": "coordinator"
  }
}
PATCH /api/v1/cases/{id}/sightings/{sighting_id}

Confirm or discard a sighting

Requires a responder access token.

status is confirmed or discarded. Only confirmed sightings steer a search, and the first one moves an open case to sighted. docs/CASES_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 PATCH "https://your-account.alertroster.com/api/v1/cases/{id}/sightings/{sighting_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "confirmed"
}'

Response 200

{
  "sighting": {
    "id": "40aa…",
    "status": "confirmed"
  }
}

routing

What raises events, and the ladders an unanswered incident walks. See docs/ROUTING_API.md in the repository.

GET /api/v1/escalation_policies

Every escalation ladder in the account

Requires a responder access token.

Each policy with its rungs in position order, lowest first, and what each rung names: a person, or a rota to ask when it fires. repeat_count is how many times the whole list runs again after its last rung, so 0 runs the rungs once. Nobody is resolved here: the detail route answers who a rung would actually wake, because that is a read of the rota per rung. Admin-only, for the reason GET /api/v1/sources gives. docs/ROUTING_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 GET "https://your-account.alertroster.com/api/v1/escalation_policies" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "escalation_policies": [
    {
      "id": "7b0e…",
      "name": "Platform, out of hours",
      "rules": [
        {
          "id": "2d55…",
          "position": 0,
          "targets": [
            {
              "position": 0,
              "type": "user",
              "user_id": "9a2b…"
            },
            {
              "position": 1,
              "type": "schedule",
              "schedule_id": "5e21…"
            }
          ],
          "ack_timeout_seconds": 300
        }
      ],
      "repeat_count": 1
    }
  ]
}
GET /api/v1/escalation_policies/{id}

One ladder, with who each rung would wake now

Requires a responder access token.

The list route's object, plus would_wake on every rung: the people that rung would page, resolved the way a firing rung is resolved, each with the same via label the incident timeline records for a real page. It is a live answer about the present, not a record. It changes when a rota rotates or somebody is retired, and what a rung did wake is on the incident's timeline instead (docs/INCIDENT_API.md §16). Two unresolved cases are kept apart because they are different findings: no_targets is a rung that names nobody, nobody is a rung whose targets resolve to no-one today. at answers the question for another instant ("is this staffed on Sunday night?"), and a malformed one is a 422 rather than a silent answer about now. docs/ROUTING_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 GET "https://your-account.alertroster.com/api/v1/escalation_policies/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "escalation_policy": {
    "id": "7b0e…",
    "name": "Platform, out of hours",
    "rules": [
      {
        "id": "2d55…",
        "position": 0,
        "targets": [
          {
            "position": 0,
            "type": "schedule",
            "schedule_id": "5e21…"
          }
        ],
        "ack_timeout_seconds": 300,
        "would_wake": {
          "users": [
            {
              "via": "schedule:5e21…:layer:1",
              "user_id": "9a2b…"
            }
          ],
          "unresolved": null
        }
      }
    ],
    "resolved_at": "2026-09-17T18:04:11Z",
    "repeat_count": 1
  }
}
GET /api/v1/sources

Every source in the account

Requires a responder access token.

What raises events here, and what each one escalates to: a schedule, an escalation policy, or nothing, which is the loud fallback that pages every responder. Read-only and admin-only: this is the account's ingest configuration, and the page it mirrors is behind the same gate. Each source carries its integration keys as a prefix, a lane and two stamps, enough to tell two keys apart and to spot a monitor that has gone quiet. Never the key itself: the raw string exists once, at the moment it is minted. docs/ROUTING_API.md §2. 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/sources" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "sources": [
    {
      "id": "3f8a…",
      "name": "Prometheus (prod)",
      "type": "monitor",
      "max_priority": "emergency",
      "default_local_grace_seconds": 0,
      "default_priority": "normal",
      "default_sound": "mail",
      "escalation_policy_id": "7b0e…",
      "escalation_schedule_id": null,
      "integration_keys": [
        {
          "id": "c4d2…",
          "last_used_at": "2026-09-17T18:02:44Z",
          "revoked_at": null,
          "key_prefix": "ark_async_9Xq2TbLm",
          "lane": "async"
        }
      ],
      "escalates_to": {
        "id": "7b0e…",
        "type": "escalation_policy"
      }
    }
  ]
}

records

What happened over a range: the closed incidents, the coverage report, the raw export. See docs/RECORDS.md in the repository.

GET /api/v1/records/coverage

The coverage report over a date range

Requires a responder access token.

Was somebody on call, did people check in, and how fast did anyone answer, across the range, as JSON. The same object the page renders. Every figure is a count of rows written at the time, never a simulation of the current rota over past dates: unrostered_pages counts the moments this system needed a responder and resolved to nobody, which is a narrower claim than "the rota had no gaps" and is the true one. Three nulls mean something specific and a caller that flattens them will report the opposite of the truth: compliance_rate: null is "no deadlines fell in this range", not zero percent. docs/RECORDS.md §8 documents each. Same range rules, same gates, as the incident list. 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/records/coverage" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "coverage": {
    "expired": 1,
    "from": "2026-09-01T00:00:00Z",
    "to": "2026-09-30T23:59:59Z",
    "incidents": 14,
    "acknowledged": 13,
    "generated_at": "2026-10-01T06:00:00Z",
    "compliance_percent": 97.8,
    "compliance_rate": 0.978,
    "ack_seconds_median": 47,
    "ack_seconds_worst": 512,
    "checkins_cancelled": 3,
    "checkins_extended": 6,
    "checkins_missed": 2,
    "checkins_satisfied": 88,
    "rostered_pages": 19,
    "unacknowledged": 1,
    "unrostered": [
      {
        "at": "2026-09-12T02:14:00Z",
        "kind": "escalated",
        "target": "nobody",
        "incident_id": "1d0c…"
      }
    ],
    "unrostered_pages": 1
  }
}
GET /api/v1/records/export

The raw append-only rows, as a file

Requires a responder access token.

The rows themselves, for a customer's own retention or BI, byte for byte the file the download button on /records produces, from the same encoder and the same window. format is required and is one of json (both tables in one document), timeline_csv (the incident timeline) or checkin_csv (the check-in events). Two files rather than one because the two tables have different columns and a merged CSV would be mostly empty cells; docs/RECORDS.md §6 names every column. The response carries content-disposition with a filename that includes the range, and answers text/csv for the two CSV formats. Ask for one with Accept: text/csv if you like, which this route accepts and the JSON-only routes beside it do not. Same range rules and same gates as the two reads above. 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/records/export" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v1/records/incidents

The incidents raised in a date range

Requires a responder access token.

What the records page lists, as JSON: every incident raised between from and to, newest first, with when it was raised, answered and closed, who answered, what it closed as (and expired is the finding), what raised it, and how far up the ladder it got. from and to are dates, taken as whole days in UTC at both ends, so what a person looked at on /records and what a workflow fetched are the same period. Both are required: "the last seven days" is a decision a caller makes out loud rather than inherits from a server's default. Incidents are counted by when they were raised, so two adjacent months add up. Paged with limit and offset; has_more says whether another page exists. A range longer than 366 days is refused rather than truncated. Admin-only, and at the API tier rather than the records tier: the page is Lone Worker and the systems layer is Duty of Care, which is docs/RECORDS.md §5. §8 is this route's contract. 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/records/incidents" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "offset": 0,
  "from": "2026-09-01T00:00:00Z",
  "limit": 100,
  "to": "2026-09-30T23:59:59Z",
  "has_more": false,
  "incidents": [
    {
      "id": "4c1f…",
      "priority": "emergency",
      "status": "resolved",
      "title": "Overdue: Evening walk",
      "source_id": "3f8a…",
      "checkin_id": "8b7e…",
      "urgency": "high",
      "resolved_at": "2026-09-17T19:40:00Z",
      "triggered_at": "2026-09-17T18:04:11Z",
      "acknowledged_at": "2026-09-17T18:05:02Z",
      "acknowledged_by_user_id": "9a2b…",
      "escalation_rule_position": 1,
      "escalation_repeat_count": 0
    }
  ]
}

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"
GET /api/v1/users/me/response_notes

Read what you share with your response team

Requires a responder access token.

Free text a responder wants their response team to know: a membership number and its incident line, an attorney, medical notes. text is null when nothing is set. Takes a stale token, because an app caches this for an SOS it may declare with no signal. docs/DEVICE_API.md §8b.

Request

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

Response 200

{
  "response_notes": {
    "text": "USCCA member 1234567, incident line 1-877-...",
    "max_length": 1000,
    "updated_at": "2026-09-19T19:00:00Z"
  }
}
PUT /api/v1/users/me/response_notes

Set what you share with your response team

Requires a responder access token.

Replaces the text. A blank text deletes it. Encrypted at rest; shown to your account's responders only on an incident about you, and sent to Garmin Response when your app declares an SOS on your paired inReach. 422 invalid_request over max_length; 503 encryption_unavailable when the deployment has no key (clearing still works). docs/DEVICE_API.md §8b. 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/users/me/response_notes" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "USCCA member 1234567, incident line 1-877-..."
}'

Response 200

{
  "response_notes": {
    "text": "USCCA member 1234567, incident line 1-877-...",
    "max_length": 1000,
    "updated_at": "2026-09-19T19:00:00Z"
  }
}

Team chat

Team chat: channels, DMs, groups, and an incident's thread as a conversation. See docs/CHAT_API.md in the repository.

POST /api/v2/chat/broadcasts

Send a must-acknowledge broadcast

Requires a responder access token.

An account admin. conversation_id (a channel or group: its people) or user_group_id (its members, each in a direct message from the AlertRoster bot), body, ack_within_minutes (1 to 10080) and escalate: none, each_person (an incident handed to each person who has not acknowledged by then) or policy with policy_id (one incident on that ladder naming them). A person's token only. docs/CHAT_API.md §25.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/broadcasts" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Radio check at 0600, all units",
  "conversation_id": "c41d…",
  "escalate": "each_person",
  "ack_within_minutes": 30
}'

Response 201

{
  "broadcast": {
    "id": "b7e2…",
    "body": "Radio check at 0600, all units",
    "recipients": [
      {
        "name": "Sam Reyes",
        "user_id": "9a2b…",
        "incident_id": null,
        "acknowledged_at": null
      }
    ],
    "ack_by": "2026-10-24T06:00:00Z",
    "escalate": "each_person"
  }
}
GET /api/v2/chat/broadcasts/{broadcast_id}

Who has acknowledged a broadcast

Requires a responder access token.

An account admin: each recipient, when they acknowledged (null if they have not), and the incident they were escalated with. docs/CHAT_API.md §25.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/broadcasts/{broadcast_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "broadcast": {
    "id": "b7e2…",
    "recipients": [
      {
        "name": "Sam Reyes",
        "user_id": "9a2b…",
        "incident_id": null,
        "acknowledged_at": "…"
      }
    ]
  }
}
GET /api/v2/chat/channels

Browse the account's public channels

Requires a responder access token.

Every public channel, whether or not you are in it, for finding one to join. include_archived=true adds archived ones. docs/CHAT_API.md §4.3.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/channels" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "conversations": [
    {
      "id": "c41d…",
      "name": "ops",
      "membership": {
        "role": "owner",
        "joined_at": "2026-10-12T14:00:00Z",
        "muted": false,
        "notification_level": "mentions"
      },
      "kind": "channel",
      "topic": "North ridge search",
      "purpose": "",
      "created_by_user_id": "9a2b…",
      "incident_id": null,
      "archived_at": null,
      "archived": false,
      "created_at": "2026-10-12T14:00:00Z",
      "default_kind": null,
      "site_feature_id": null,
      "visibility": "public",
      "member_user_ids": null,
      "member_count": 4
    }
  ]
}
GET /api/v2/chat/commands

The slash commands you may run

Requires a responder access token.

The built-ins first, then each installed app's, with its usage, for a composer's / menu. A person's token only. docs/CHAT_API.md §22.6.

Request

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

Response 200

{
  "commands": [
    {
      "name": "page",
      "description": "Page a ladder or a person",
      "usage": "/page <ladder> <message>  or  /page @person <message>",
      "app": null
    },
    {
      "name": "triage",
      "description": "Open a triage report",
      "usage": "/triage [what]",
      "app": {
        "id": "71cd…",
        "name": "Triage bot"
      }
    }
  ]
}
GET /api/v2/chat/conversations

The conversations you are in

Requires a responder access token.

Channels, DMs and groups you are in now, with your membership on each. Never incident threads, which are reached from the incident. kind narrows to one kind; include_archived=true adds archived channels. docs/CHAT_API.md §4.1.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "conversations": [
    {
      "id": "c41d…",
      "name": "ops",
      "membership": {
        "role": "owner",
        "joined_at": "2026-10-12T14:00:00Z",
        "muted": false,
        "notification_level": "mentions"
      },
      "kind": "channel",
      "topic": "North ridge search",
      "purpose": "",
      "created_by_user_id": "9a2b…",
      "incident_id": null,
      "archived_at": null,
      "archived": false,
      "created_at": "2026-10-12T14:00:00Z",
      "default_kind": null,
      "site_feature_id": null,
      "visibility": "public",
      "member_user_ids": null,
      "member_count": 4
    }
  ]
}
POST /api/v2/chat/conversations

Create a channel, open a DM or start a group

Requires a responder access token.

kind channel with name (1 to 80 lowercase letters, digits, hyphens or underscores; a leading # and capitals are folded), visibility (public or private), optional topic, purpose and user_ids to add; dm with the one other person in user_ids; or group with user_ids, three to nine people with you. 201 when created; a DM that already exists answers 200 with it. docs/CHAT_API.md §4.2.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ops",
  "kind": "channel",
  "topic": "North ridge search",
  "visibility": "public"
}'

Response 201

{
  "conversation": {
    "id": "c41d…",
    "name": "ops",
    "membership": {
      "role": "owner",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    },
    "kind": "channel",
    "topic": "North ridge search",
    "purpose": "",
    "created_by_user_id": "9a2b…",
    "incident_id": null,
    "archived_at": null,
    "archived": false,
    "created_at": "2026-10-12T14:00:00Z",
    "default_kind": null,
    "site_feature_id": null,
    "visibility": "public",
    "member_user_ids": null,
    "member_count": 4
  }
}
GET /api/v2/chat/conversations/{id}

Read a conversation

Requires a responder access token.

A public channel, one you are in, or an incident's you can see. Anything else is 404. docs/CHAT_API.md §4.5.

Request

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

Response 200

{
  "conversation": {
    "id": "c41d…",
    "name": "ops",
    "membership": {
      "role": "owner",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    },
    "kind": "channel",
    "topic": "North ridge search",
    "purpose": "",
    "created_by_user_id": "9a2b…",
    "incident_id": null,
    "archived_at": null,
    "archived": false,
    "created_at": "2026-10-12T14:00:00Z",
    "default_kind": null,
    "site_feature_id": null,
    "visibility": "public",
    "member_user_ids": null,
    "member_count": 4
  }
}
PATCH /api/v2/chat/conversations/{id}

Rename, or set the topic or purpose

Requires a responder access token.

name (a channel's, by its owner or an admin of it, or an account admin; never #general), topic and purpose (up to 250 characters, by any member but a guest). Each change is a system message in the conversation. docs/CHAT_API.md §4.6.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/conversations/{id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ops-north",
  "topic": "North ridge search, sector 4"
}'

Response 200

{
  "conversation": {
    "id": "c41d…",
    "name": "ops",
    "membership": {
      "role": "owner",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    },
    "kind": "channel",
    "topic": "North ridge search",
    "purpose": "",
    "created_by_user_id": "9a2b…",
    "incident_id": null,
    "archived_at": null,
    "archived": false,
    "created_at": "2026-10-12T14:00:00Z",
    "default_kind": null,
    "site_feature_id": null,
    "visibility": "public",
    "member_user_ids": null,
    "member_count": 4
  }
}
POST /api/v2/chat/conversations/{id}/archive

Archive a channel

Requires a responder access token.

By its owner or an admin of it, or an account admin. Never #general. An archived channel is read-only until unarchived: posts and changes answer 409 archived. docs/CHAT_API.md §4.7.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/archive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "conversation": {
    "id": "c41d…",
    "archived": true
  }
}
POST /api/v2/chat/conversations/{id}/commands

Run a slash command

Requires a responder access token.

text is the whole command, /page Night shift generator down; mentions the composer's spans over it and thread_root_id the thread it was typed in, both optional. /page, /ack and /resolve raise, acknowledge and resolve exactly as the incident API does, as you. The answer has the ephemeral shown only to you (also pushed to your sockets with the same id), a message posted, or a modal view opened. A command whose effect failed is 422 command_failed with the reason, never a 200. A person's token only. docs/CHAT_API.md §22.6.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/commands" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "/page Night shift generator 2 is down",
  "mentions": []
}'

Response 200

{
  "closed": false,
  "message": {
    "id": "8e2a…",
    "body": "Dana paged Night shift (Sam): generator 2 is down"
  },
  "view": null,
  "ephemeral": null
}
GET /api/v2/chat/conversations/{id}/files

A conversation's files

Requires a responder access token.

Every file posted in it, newest first, for its files browser. kind (image, video, audio, pdf, file), user_id, and before (a file id) with limit (1 to 200, default 50) to page. docs/CHAT_API.md §20.7.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/files" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "files": [
    {
      "id": "7d1c…",
      "filename": "ridge.jpg",
      "kind": "image",
      "thumbnails": [
        "thumb_64"
      ]
    }
  ],
  "has_more": false,
  "conversation_id": "0b6f…"
}
POST /api/v2/chat/conversations/{id}/join

Join a public channel

Requires a responder access token.

Any member of the account. Already in it answers the same. A private channel is joined only by invitation. docs/CHAT_API.md §4.8.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/join" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "conversation": {
    "id": "c41d…",
    "name": "ops",
    "membership": {
      "role": "owner",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    },
    "kind": "channel",
    "topic": "North ridge search",
    "purpose": "",
    "created_by_user_id": "9a2b…",
    "incident_id": null,
    "archived_at": null,
    "archived": false,
    "created_at": "2026-10-12T14:00:00Z",
    "default_kind": null,
    "site_feature_id": null,
    "visibility": "public",
    "member_user_ids": null,
    "member_count": 4
  }
}
POST /api/v2/chat/conversations/{id}/leave

Leave a channel or a group

Requires a responder access token.

Not #general (422 general_channel), and not a DM (422 unsupported). docs/CHAT_API.md §4.8.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/leave" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "conversation": {
    "id": "c41d…",
    "name": "ops",
    "membership": {
      "role": "owner",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    },
    "kind": "channel",
    "topic": "North ridge search",
    "purpose": "",
    "created_by_user_id": "9a2b…",
    "incident_id": null,
    "archived_at": null,
    "archived": false,
    "created_at": "2026-10-12T14:00:00Z",
    "default_kind": null,
    "site_feature_id": null,
    "visibility": "public",
    "member_user_ids": null,
    "member_count": 4
  }
}
GET /api/v2/chat/conversations/{id}/members

Who is in a conversation

Requires a responder access token.

The members now, oldest first, with their role and notification level. An incident's conversation has none: who it is for is the incident's participants (docs/INCIDENT_API.md §23.5). docs/CHAT_API.md §4.9.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/members" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "members": [
    {
      "role": "owner",
      "user_id": "9a2b…",
      "added_by_user_id": "9a2b…",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    }
  ],
  "conversation_id": "c41d…"
}
POST /api/v2/chat/conversations/{id}/members

Add somebody to a channel or a group

Requires a responder access token.

user_id of a member of the account, by any member but a guest. A group stays within nine people. Already in it answers the same. docs/CHAT_API.md §4.9.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/members" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "user_id": "5f61…"
}'

Response 200

{
  "members": [
    {
      "role": "owner",
      "user_id": "9a2b…",
      "added_by_user_id": "9a2b…",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    }
  ],
  "conversation_id": "c41d…"
}
PATCH /api/v2/chat/conversations/{id}/members/{user_id}

Change a channel member's role

Requires a responder access token.

role admin, member or guest, by the channel's owner or an admin of it, or an account admin. The owner's role is not changed here. docs/CHAT_API.md §4.9.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/members/{user_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "role": "admin"
}'

Response 200

{
  "members": [
    {
      "role": "owner",
      "user_id": "9a2b…",
      "added_by_user_id": "9a2b…",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    }
  ],
  "conversation_id": "c41d…"
}
DELETE /api/v2/chat/conversations/{id}/members/{user_id}

Remove somebody from a channel

Requires a responder access token.

By the channel's owner or an admin of it, or an account admin. Never from #general. docs/CHAT_API.md §4.9.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/members/{user_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "members": [
    {
      "role": "owner",
      "user_id": "9a2b…",
      "added_by_user_id": "9a2b…",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": false,
      "notification_level": "mentions"
    }
  ],
  "conversation_id": "c41d…"
}
PATCH /api/v2/chat/conversations/{id}/membership

Change your notifications in a conversation, and your sidebar

Requires a responder access token.

Your own. notification_level all, mentions or none; muted true (until you unmute) or false (clears any mute); muted_until an ISO 8601 time to mute until then. Muting quiets chat only; it never touches paging. starred, section (a name of your own, at most 40 characters, null to take it out of one) and section_position arrange your sidebar. A membership update on your stream tells your other devices. Answers the conversation. 403 when you are not in it, 422 unsupported on an incident's conversation. docs/CHAT_API.md §11.1.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/membership" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "muted_until": "2026-10-21T07:00:00Z",
  "notification_level": "all"
}'

Response 200

{
  "conversation": {
    "id": "c41d…",
    "name": "ops",
    "membership": {
      "role": "member",
      "section": "Field",
      "joined_at": "2026-10-12T14:00:00Z",
      "muted": true,
      "muted_until": "2026-10-21T07:00:00Z",
      "notification_level": "all",
      "section_position": 0,
      "starred": true
    },
    "kind": "channel"
  }
}
GET /api/v2/chat/conversations/{id}/messages

Read a conversation's messages

Requires a responder access token.

Oldest first, after after_seq (exclusive), or the latest limit before before_seq to open at the end and scroll back, at most limit (default 200, max 500), with has_more: the message object of docs/INCIDENT_API.md §23.2, plus conversation_id and, on a system message, event. seq rises within a conversation but is not dense. docs/CHAT_API.md §5.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "messages": [
    {
      "id": "8e2a…",
      "seq": 41,
      "body": "Sector 4 is clear.",
      "kind": "text",
      "event": null,
      "incident_id": null,
      "conversation_id": "c41d…"
    }
  ],
  "has_more": false,
  "conversation_id": "c41d…"
}
POST /api/v2/chat/conversations/{id}/messages

Post to a conversation

Requires a responder access token.

As docs/INCIDENT_API.md §23.1: client_message_id makes it idempotent (201 new, 200 a resend), kind is text, location or location_request, and where you were rides along. In an incident's conversation this is a post to the incident's thread. Elsewhere you must be in the conversation (403), and it must not be archived (409). The answer carries the message's pts in the stream, for an optimistic client to reconcile. Optional: mentions (structured spans, §13), thread_root_id with also_send_to_conversation (a reply, §12), forward_of_message_id (§17). Rich messages (§19), in a channel, DM or group only: kind poll with poll, checklist with checklist, voice_note with voice_note_id; blocks and file_ids (up to 10 completed uploads of yours, §20.2) on a text; live on a location. Anything with no words for a plain leg is 422. docs/CHAT_API.md §5.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Sector 4 is clear.",
  "kind": "text",
  "client_message_id": "a1b2c3d4-5e6f-…"
}'

Response 201

{
  "message": {
    "id": "8e2a…",
    "seq": 41,
    "kind": "text"
  },
  "pts": 1043
}
PATCH /api/v2/chat/conversations/{id}/messages/{message_id}

Edit a message

Requires a responder access token.

body and optional mentions. The author only, within the account's editing window (409 edit_window_passed), never a system message (422 unsupported), a deleted one (409 deleted) or an incident's (422 unsupported). Every earlier version is kept; new mentions notify nobody. docs/CHAT_API.md §15. With blocks (and body), it is not an edit but an update in place, the way an app changes its own card: no editing window, never shown as edited, every version still kept. docs/CHAT_API.md §22.7.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Sector 4 is clear, both teams out."
}'

Response 200

{
  "message": {
    "id": "8e2a…",
    "thread": null,
    "seq": 41,
    "body": "Sector 4 is clear.",
    "deleted": false,
    "pinned": false,
    "reactions": [
      {
        "count": 2,
        "emoji": "👍",
        "user_ids": [
          "9a2b…",
          "5f61…"
        ]
      }
    ],
    "edited_at": null
  }
}
DELETE /api/v2/chat/conversations/{id}/messages/{message_id}

Delete a message for its readers

Requires a responder access token.

Its author, or where the account allows it an account admin or the channel's owner or an admin of it. Readers see a tombstone; the message and every version stay on the record. docs/CHAT_API.md §15.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "body": "",
    "deleted": true
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/actions

Press a button, choose an option or a date

Requires a responder access token.

action_id of an element the message shows now, with selected_option (a select's value) or selected_date (YYYY-MM-DD). A /page card's Acknowledge and Resolve are answered here, as you; anything else an app posted is sent to that app, signed, and its answer may update the message in place, open a modal or say something to you. Answered as a command is. Members only. A person's token only. docs/CHAT_API.md §22.7.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/actions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "action_id": "take"
}'

Response 200

{
  "closed": false,
  "message": {
    "id": "8e2a…",
    "body": "Sam took pump-2",
    "edited_at": null
  },
  "view": null,
  "ephemeral": null
}
GET /api/v2/chat/conversations/{id}/messages/{message_id}/audio

A voice note's audio

Requires a responder access token.

A short-lived URL to GET the audio of a voice_note message you can read. Mint one per play; never store it. 503 media_unavailable when no media bucket is configured. docs/CHAT_API.md §19.7.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/audio" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "url": "https://…",
  "expires_at": "2026-10-14T14:36:50Z"
}
PUT /api/v2/chat/conversations/{id}/messages/{message_id}/checklist/items/{index}

Tick a checklist item

Requires a responder access token.

Any member but a guest. index from 0. Ticked already is the same answer. docs/CHAT_API.md §19.5.

Request

curl -X PUT "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/checklist/items/{index}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "kind": "checklist",
    "checklist": {
      "title": "Vehicle check",
      "items": [
        {
          "index": 0,
          "text": "Fuel",
          "checked": true,
          "checked_by": "9a2b…"
        }
      ]
    }
  }
}
DELETE /api/v2/chat/conversations/{id}/messages/{message_id}/checklist/items/{index}

Untick a checklist item

Requires a responder access token.

Any member but a guest. docs/CHAT_API.md §19.5.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/checklist/items/{index}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "checklist": {
      "items": [
        {
          "index": 0,
          "checked": false
        }
      ]
    }
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/follow

Follow a thread

Requires a responder access token.

You are pushed its replies. A thread's root only. docs/CHAT_API.md §12.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/follow" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "thread": {
      "following": true
    }
  }
}
DELETE /api/v2/chat/conversations/{id}/messages/{message_id}/follow

Stop following a thread

Requires a responder access token.

docs/CHAT_API.md §12.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/follow" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "thread": {
      "following": false
    }
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/incident

Raise an incident from a message

Requires a responder access token.

policy_id (a ladder) or user_id (a person), and an optional title (the message's words otherwise). It raises exactly as /page does, as you: rung 0 of the ladder, or the incident handed to the person. The incident carries chat_message_id, its record a chat_raised_from entry with the message and its thread as they read now, and its live card is posted in the message's thread. A refusal (a ladder with no rungs, a person nothing can page) is 422 not_raised with the reason, and nothing is raised. A person's token only. docs/CHAT_API.md §24.3.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/incident" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Pump 2 leaking into bay 3",
  "policy_id": "5c1e…"
}'

Response 201

{
  "message": {
    "id": "8e2a…",
    "thread_root_id": "7d10…"
  },
  "reached": "Primary (Sam Reyes)",
  "incident": {
    "id": "a6f0…",
    "status": "triggered",
    "title": "Pump 2 leaking into bay 3"
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/pin

Pin a message to its conversation

Requires a responder access token.

Any member but a guest; said in the conversation as a system message. docs/CHAT_API.md §16.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/pin" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "pinned": true
  }
}
DELETE /api/v2/chat/conversations/{id}/messages/{message_id}/pin

Unpin a message

Requires a responder access token.

Any member but a guest. docs/CHAT_API.md §16.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/pin" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "pinned": false
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/poll/close

Close a poll

Requires a responder access token.

Its author, an account admin, or the channel's owner or an admin of it. Closing a closed poll changes nothing. docs/CHAT_API.md §19.4.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/poll/close" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "poll": {
      "closed": true,
      "closed_at": "…"
    }
  }
}
PUT /api/v2/chat/conversations/{id}/messages/{message_id}/poll/vote

Vote in a poll

Requires a responder access token.

options: the indexes you choose, replacing any earlier vote; [] takes it back. One unless the poll is multiple. A closed poll is 409 poll_closed. docs/CHAT_API.md §19.4.

Request

curl -X PUT "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/poll/vote" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "options": [
    1
  ]
}'

Response 200

{
  "message": {
    "id": "8e2a…",
    "kind": "poll",
    "poll": {
      "closed": false,
      "options": [
        {
          "count": 0,
          "index": 0,
          "text": "Me"
        },
        {
          "count": 1,
          "index": 1,
          "text": "Not me"
        }
      ],
      "question": "Who can cover the 0600 handoff?",
      "my_votes": [
        1
      ]
    }
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/positions

Post a point of your live location share

Requires a responder access token.

The share's author only. Once the share is over (its time ran out, or it was stopped) the answer is 409 stopped with a stop object naming the message, reason (expired or stopped) and at: stop the position session then, and only then. A point captured before at is still kept. The same pattern as docs/INCIDENT_API.md §21. docs/CHAT_API.md §19.6.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/positions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "latitude": 47.75,
  "longitude": -121.85,
  "accuracy_m": 12,
  "fix_at": "2026-10-14T14:31:50Z"
}'

Response 200

{
  "message": {
    "id": "8e2a…",
    "live": {
      "last": {
        "latitude": 47.75
      },
      "stopped": false,
      "until": "…"
    }
  }
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/reactions

React to a message

Requires a responder access token.

emoji: one Unicode emoji, or :name: of one of the account's custom emoji. Members only. Already there is the same answer. docs/CHAT_API.md §14.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/reactions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "emoji": "👍"
}'

Response 200

{
  "message": {
    "id": "8e2a…",
    "thread": null,
    "seq": 41,
    "body": "Sector 4 is clear.",
    "deleted": false,
    "pinned": false,
    "reactions": [
      {
        "count": 2,
        "emoji": "👍",
        "user_ids": [
          "9a2b…",
          "5f61…"
        ]
      }
    ],
    "edited_at": null
  }
}
DELETE /api/v2/chat/conversations/{id}/messages/{message_id}/reactions/{emoji}

Take a reaction back

Requires a responder access token.

emoji in the path, URL-encoded. Not there is the same answer. docs/CHAT_API.md §14.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/reactions/{emoji}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "thread": null,
    "seq": 41,
    "body": "Sector 4 is clear.",
    "deleted": false,
    "pinned": false,
    "reactions": [
      {
        "count": 2,
        "emoji": "👍",
        "user_ids": [
          "9a2b…",
          "5f61…"
        ]
      }
    ],
    "edited_at": null
  }
}
GET /api/v2/chat/conversations/{id}/messages/{message_id}/revisions

Every version of a message

Requires a responder access token.

The original and each edit, oldest first. A deleted message's history is an account admin's to read (403 otherwise). docs/CHAT_API.md §15.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/revisions" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message_id": "8e2a…",
  "revisions": [
    {
      "at": "…",
      "body": "Sector 4 clear",
      "by_user_id": "9a2b…"
    }
  ]
}
GET /api/v2/chat/conversations/{id}/messages/{message_id}/seen

Who has seen a message

Requires a responder access token.

seen_by_count: the members besides its author whose read marker is at or past it. seen_by_user_ids only in a DM or a group whose account shows read receipts, otherwise null. Members only (403). docs/CHAT_API.md §11.5.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/seen" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message_id": "8e2a…",
  "seen_by_count": 2,
  "seen_by_user_ids": [
    "5f61…",
    "77c0…"
  ]
}
POST /api/v2/chat/conversations/{id}/messages/{message_id}/stop

Stop your live location share

Requires a responder access token.

The share's author only. Your other devices are told with a silent chat_location_stop push. docs/CHAT_API.md §19.6.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/stop" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "message": {
    "id": "8e2a…",
    "live": {
      "stopped": true,
      "stop_reason": "stopped"
    }
  }
}
GET /api/v2/chat/conversations/{id}/messages/{message_id}/thread

A thread: its root and replies

Requires a responder access token.

Replies oldest first after after_seq, at most limit (default 200, max 500), with has_more. 404 when the message is not a root here. docs/CHAT_API.md §12.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/messages/{message_id}/thread" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "root": {
    "id": "8e2a…",
    "thread": {
      "following": true,
      "reply_count": 2
    }
  },
  "replies": [
    {
      "id": "91c0…",
      "thread_root_id": "8e2a…"
    }
  ],
  "has_more": false,
  "conversation_id": "c41d…"
}
GET /api/v2/chat/conversations/{id}/pins

A conversation's pinned messages

Requires a responder access token.

Most recently pinned first, as the message object of §5.1. docs/CHAT_API.md §16.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/pins" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "messages": [
    {
      "id": "8e2a…",
      "pinned": true
    }
  ],
  "conversation_id": "c41d…"
}
POST /api/v2/chat/conversations/{id}/read

Mark a conversation read

Requires a responder access token.

seq: the last message you have seen. Only ever moves forward and never past the last message; an older seq changes nothing. A move is a read update on your own stream, so your other devices clear the badge. 403 when you are not in it, 422 unsupported on an incident's conversation, 422 invalid_request without a valid seq. docs/CHAT_API.md §4.11.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/read" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "seq": 41
}'

Response 200

{
  "conversation_id": "c41d…",
  "last_read_seq": 41
}
POST /api/v2/chat/conversations/{id}/unarchive

Unarchive a channel

Requires a responder access token.

By its owner or an admin of it, or an account admin. docs/CHAT_API.md §4.7.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/conversations/{id}/unarchive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "conversation": {
    "id": "c41d…",
    "archived": false
  }
}
GET /api/v2/chat/emoji

The account's custom emoji

Requires a responder access token.

Reacted with as :name:. docs/CHAT_API.md §14.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/emoji" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "emoji": [
    {
      "name": "ack",
      "image_url": "https://…/ack.png"
    }
  ]
}
POST /api/v2/chat/emoji

Add a custom emoji

Requires a responder access token.

An account admin. name (1 to 32 lowercase letters, digits, _, + or -) and an https image_url. docs/CHAT_API.md §14.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/emoji" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ack",
  "image_url": "https://example.com/ack.png"
}'

Response 201

{
  "name": "ack",
  "image_url": "https://example.com/ack.png"
}
DELETE /api/v2/chat/emoji/{name}

Remove a custom emoji

Requires a responder access token.

An account admin. Reactions with it stay, as text. docs/CHAT_API.md §14.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/emoji/{name}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v2/chat/files/{file_id}/url

A file's download URL

Requires a responder access token.

A short-lived signed URL for a file on a message you can read, or one of your own not yet posted. variant is original (the default; a photo's copy without its metadata) or thumb_64, thumb_320, thumb_1024. 404 for a file you cannot read, 409 scan_pending until the malware scan passes, 409 processing for a thumbnail not made yet, 410 infected. Mint one per view. docs/CHAT_API.md §20.4.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/files/{file_id}/url" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "url": "https://…",
  "expires_at": "2026-10-20T14:36:50Z",
  "variant": "thumb_320",
  "file_id": "7d1c…"
}
GET /api/v2/chat/incidents/{incident_id}

An incident's thread, as a conversation

Requires a responder access token.

The conversation of an incident you can see, made the first time it is asked for. Its messages are the incident's thread, read and posted through /api/v2/chat/conversations/:id/messages or through docs/INCIDENT_API.md §23 alike. 404 for an incident you cannot see. docs/CHAT_API.md §4.4.

Request

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

Response 200

{
  "conversation": {
    "id": "e7a0…",
    "kind": "incident",
    "incident_id": "4c1f…"
  }
}
GET /api/v2/chat/me

Your Do Not Disturb, away and status

Requires a responder access token.

dnd.active says whether Do Not Disturb is in force now, from dnd.until (a snooze) or dnd.schedule (a weekly window in your time zone). While it is, a chat push still comes, quietly: no sound, nothing lit. It never quiets a page: an incident wakes you whatever this says. status is null once it has expired. docs/CHAT_API.md §11.2.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/me" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "status": {
    "text": "On a call",
    "expires_at": "2026-10-20T18:00:00Z",
    "emoji": "🚒"
  },
  "away": false,
  "dnd": {
    "active": false,
    "until": null,
    "schedule": {
      "start": "22:00",
      "end": "07:00",
      "timezone": "America/Detroit",
      "days": [
        1,
        2,
        3,
        4,
        5
      ]
    }
  }
}
PATCH /api/v2/chat/me

Change your Do Not Disturb, away and status

Requires a responder access token.

Each field optional. dnd_until an ISO 8601 time or null; dnd_schedule {timezone, start, end, days} (HH:MM, crossing midnight when end is not after start; days ISO weekdays the window starts on, 1 is Monday) or null; away a boolean; status {emoji, text, expires_at} (at most 64 and 100 characters) or null. Away and status are pushed to everybody's chat socket. Answers as GET. docs/CHAT_API.md §11.2.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/me" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "status": {
    "text": "On a call",
    "emoji": "🚒"
  },
  "dnd_until": "2026-10-20T22:00:00Z"
}'

Response 200

{
  "status": {
    "text": "On a call",
    "expires_at": null,
    "emoji": "🚒"
  },
  "away": false,
  "dnd": {
    "active": true,
    "until": "2026-10-20T22:00:00Z",
    "schedule": null
  }
}
GET /api/v2/chat/presence

Who is around

Requires a responder access token.

For each of user_ids (comma-separated, at most 500, the account's people; anybody else is left out): state active (holding a chat stream, not set away) or away, on_call (a schedule resolves to them now) and status. Live changes come on the chat socket as presence and status pushes. docs/CHAT_API.md §11.4.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/presence" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "presence": [
    {
      "status": null,
      "state": "active",
      "user_id": "9a2b…",
      "on_call": true
    }
  ]
}
GET /api/v2/chat/saved

Your saved messages

Requires a responder access token.

Newest saved first, each with its message and reminder. docs/CHAT_API.md §16.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/saved" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "saved": [
    {
      "message": {
        "id": "8e2a…"
      },
      "remind_at": null,
      "reminded_at": null,
      "saved_at": "…"
    }
  ]
}
POST /api/v2/chat/saved

Save a message for later

Requires a responder access token.

message_id of any message you can read, an incident's included, and an optional future remind_at: an ordinary push reminds you then. Saving again moves the reminder. docs/CHAT_API.md §16.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/saved" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "message_id": "8e2a…",
  "remind_at": "2026-10-14T08:00:00Z"
}'

Response 201

{
  "message_id": "8e2a…",
  "remind_at": "2026-10-14T08:00:00Z"
}
DELETE /api/v2/chat/saved/{message_id}

Forget a saved message

Requires a responder access token.

204; not saved is the same answer. docs/CHAT_API.md §16.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/saved/{message_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
GET /api/v2/chat/settings

The account's chat settings

Requires a responder access token.

edit_window_minutes (null: no limit), delete_others (admins or nobody), the level a new membership starts at for a channel and for a DM or group, whether DMs and groups show read receipts, and how long somebody is away before their unread mentions are emailed (null: never). docs/CHAT_API.md §11 and §18.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/settings" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "default_channel_level": "mentions",
  "default_dm_level": "all",
  "delete_others": "admins",
  "digest_after_hours": 24,
  "edit_window_minutes": 60,
  "read_receipts": true
}
PATCH /api/v2/chat/settings

Change the account's chat settings

Requires a responder access token.

An account admin. edit_window_minutes 1 to 10,080 or null; delete_others admins or nobody; default_channel_level and default_dm_level all, mentions or none (what a new membership starts at; existing ones keep theirs); read_receipts a boolean; digest_after_hours 1 to 720 or null. docs/CHAT_API.md §11 and §18.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/settings" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "delete_others": "nobody",
  "edit_window_minutes": 15,
  "read_receipts": false
}'

Response 200

{
  "default_channel_level": "mentions",
  "default_dm_level": "all",
  "delete_others": "nobody",
  "digest_after_hours": 24,
  "edit_window_minutes": 15,
  "read_receipts": false
}
GET /api/v2/chat/switcher

The quick switcher

Requires a responder access token.

Conversations and people whose name, or a word of it, starts with q (a leading # or @ is ignored): channels you can open, your DMs and groups with somebody who matches, and the account's live people, at most 10 of each, your own channels first. docs/CHAT_API.md §21.5.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/switcher" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "people": [
    {
      "name": "Dana Dispatch",
      "user_id": "9a2b…"
    }
  ],
  "conversations": [
    {
      "id": "c41d…",
      "name": "ops",
      "member": true,
      "kind": "channel",
      "member_user_ids": null
    }
  ]
}
GET /api/v2/chat/updates

What you missed: the stream since a pts

Requires a responder access token.

Every update that is yours after since, in pts order, at most limit (default 500, max 1,000) with has_more, and each author named once in users. Continue from the pts answered. More than 5,000 missed, or since absent or 0, answers too_long: true with your conversations (each with last_seq) to re-sync by seq and the pts to continue from. pts rises but is not dense. docs/CHAT_API.md §9.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/updates" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "users": {
    "9a2b…": {
      "name": "Sam Searcher"
    }
  },
  "too_long": false,
  "updates": [
    {
      "message": {
        "id": "8e2a…",
        "seq": 41,
        "body": "Sector 4 is clear.",
        "kind": "text"
      },
      "kind": "message",
      "conversation_id": "c41d…",
      "target_user_id": null,
      "pts": 1043
    }
  ],
  "has_more": false,
  "pts": 1043
}
POST /api/v2/chat/uploads

Start a file upload

Requires a responder access token.

conversation_id (one you are in), filename, content_type, byte_size, and keep_location to keep a photo's position on the file. Answers the file and how to send it: a resumable multipart upload of part_count parts of part_size bytes, straight to the bucket. 413 file_too_large or 413 storage_full with the plan's limit. docs/CHAT_API.md §20.1.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/uploads" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "byte_size": 3480112,
  "filename": "ridge.jpg",
  "content_type": "image/jpeg",
  "conversation_id": "0b6f…"
}'

Response 201

{
  "file": {
    "id": "7d1c…",
    "status": "uploading",
    "kind": "image"
  },
  "upload": {
    "status": "open",
    "part_size": 8388608,
    "part_count": 1,
    "parts_received": []
  }
}
GET /api/v2/chat/uploads/{file_id}

An upload's progress

Requires a responder access token.

Your own upload, and while it is open the parts the bucket already holds, each with its etag. After a dropped link, send only the parts not listed. docs/CHAT_API.md §20.1.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/uploads/{file_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "file": {
    "id": "7d1c…",
    "status": "uploading"
  },
  "upload": {
    "status": "open",
    "part_size": 8388608,
    "part_count": 3,
    "parts_received": [
      {
        "byte_size": 8388608,
        "etag": "a54f…",
        "part_number": 1
      }
    ]
  }
}
DELETE /api/v2/chat/uploads/{file_id}

Give up an upload

Requires a responder access token.

Aborts an open upload, or removes a completed file not yet posted. 409 attached once a message carries it. docs/CHAT_API.md §20.1.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/uploads/{file_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v2/chat/uploads/{file_id}/complete

Finish a file upload

Requires a responder access token.

parts: every part, in order, with the etag its PUT answered. The file then processes (thumbnails, ThumbHash, poster, preview) and is scanned for malware. 409 invalid_parts when a part is missing: read the progress and send it. docs/CHAT_API.md §20.1.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/uploads/{file_id}/complete" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "parts": [
    {
      "etag": "a54f…",
      "part_number": 1
    }
  ]
}'

Response 200

{
  "file": {
    "id": "7d1c…",
    "status": "processing",
    "scan_status": "pending"
  }
}
POST /api/v2/chat/uploads/{file_id}/parts

URLs to send parts to

Requires a responder access token.

part_numbers, up to 100 at a time. Each URL takes one PUT of that part's bytes and answers its ETag, which completing needs. Ask as you go: the URLs are short-lived. docs/CHAT_API.md §20.1.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/uploads/{file_id}/parts" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "part_numbers": [
    2,
    3
  ]
}'

Response 200

{
  "parts": [
    {
      "method": "PUT",
      "url": "https://…",
      "expires_at": "…",
      "part_number": 2
    }
  ]
}
GET /api/v2/chat/user_groups

The account's user groups

Requires a responder access token.

Mentionable groups (@nurses), each with its members. docs/CHAT_API.md §18.

Request

curl -X GET "https://your-account.alertroster.com/api/v2/chat/user_groups" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "user_groups": [
    {
      "handle": "nurses",
      "id": "17aa…",
      "name": "Nurses",
      "user_ids": [
        "9a2b…"
      ]
    }
  ]
}
POST /api/v2/chat/user_groups

Create a user group

Requires a responder access token.

An account admin. handle (1 to 40 lowercase letters, digits, - or _), name, user_ids. Answers every group. docs/CHAT_API.md §18.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/user_groups" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "handle": "nurses",
  "name": "Nurses",
  "user_ids": [
    "9a2b…"
  ]
}'

Response 201

{
  "user_groups": [
    {
      "handle": "nurses"
    }
  ]
}
PATCH /api/v2/chat/user_groups/{group_id}

Change a user group

Requires a responder access token.

An account admin. handle, name, and user_ids to replace its members. docs/CHAT_API.md §18.

Request

curl -X PATCH "https://your-account.alertroster.com/api/v2/chat/user_groups/{group_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "user_ids": [
    "9a2b…",
    "5f61…"
  ]
}'

Response 200

{
  "user_groups": [
    {
      "handle": "nurses"
    }
  ]
}
DELETE /api/v2/chat/user_groups/{group_id}

Delete a user group

Requires a responder access token.

An account admin. Messages that mentioned it keep who it reached. docs/CHAT_API.md §18.

Request

curl -X DELETE "https://your-account.alertroster.com/api/v2/chat/user_groups/{group_id}" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v2/chat/views/{view_id}/close

Dismiss a modal

Requires a responder access token.

Closes it without submitting; the app is not told. Not open is the same answer. docs/CHAT_API.md §22.8.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/views/{view_id}/close" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
POST /api/v2/chat/views/{view_id}/submit

Submit a modal

Requires a responder access token.

values: {block_id: value} for the modal's inputs. Only the person it was opened for, within an hour, once. A required field left empty or a value the field does not take is 422 invalid_values with errors by block_id, as are the app's own errors; the modal stays open. Otherwise it closes (closed: true) or the app replaces it (view). docs/CHAT_API.md §22.8.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/views/{view_id}/submit" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "values": {
    "what": "pump 2 seal leaking",
    "sector": "north"
  }
}'

Response 200

{
  "closed": true,
  "message": null,
  "view": null,
  "ephemeral": null
}
POST /api/v2/chat/voice_notes

Start a voice note upload

Requires a responder access token.

content_type (audio/mp4, audio/aac, audio/mpeg, audio/ogg, audio/webm, audio/wav), byte_size (at most 10 MB) and duration_ms (at most 5 minutes). Answers the note and where to PUT the audio, with the headers to send. docs/CHAT_API.md §19.7.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/voice_notes" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "byte_size": 182000,
  "content_type": "audio/mp4",
  "duration_ms": 12400
}'

Response 200

{
  "upload": {
    "method": "PUT",
    "url": "https://…",
    "headers": {
      "content-type": "audio/mp4"
    },
    "expires_at": "…"
  },
  "voice_note": {
    "id": "5c0e…",
    "transcript_status": "pending"
  }
}
POST /api/v2/chat/voice_notes/{voice_note_id}/complete

Finish a voice note upload

Requires a responder access token.

Checks the uploaded audio and transcribes it, up to 15 seconds. transcript_status is then done, failed (retried in the background) or unavailable. Post the note afterwards as a voice_note message. 409 object_missing when the upload has not landed. docs/CHAT_API.md §19.7.

Request

curl -X POST "https://your-account.alertroster.com/api/v2/chat/voice_notes/{voice_note_id}/complete" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{
  "voice_note": {
    "id": "5c0e…",
    "duration_ms": 12400,
    "transcript": "Team two is at the trailhead.",
    "transcript_status": "done"
  }
}

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. priority (normal, high or emergency) says how loud the page is, in place of what payload.severity would ask for: normal notifies once without breaking Focus, high is time-sensitive once, emergency repeats until acknowledged. sound names the tone, one of chime, critical, high, mail, normal, ping; an unknown name plays the source's tone instead. Both are optional, both fall back to the source's Default priority and Sound, both are decided by the event that opens the incident, and the source's Loudest priority still caps the priority. docs/LANE_A_API.md §4d. An email bridge in Google Apps Script, paging quietly with a mail tone: ``js UrlFetchApp.fetch('https://alertroster.com/api/v2/enqueue', { method: 'post', contentType: 'application/json', headers: { Authorization: 'Bearer ' + KEY }, payload: JSON.stringify({ event_action: 'trigger', dedup_key: 'email:' + thread.getId(), priority: 'normal', sound: 'mail', payload: { summary: subject, severity: 'info' } }) }); ``

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 '{
  "priority": "emergency",
  "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",
  "sound": "critical",
  "event_action": "trigger"
}'

Response 202

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

referral-leads

People who asked about AlertRoster on an instructor's offer, posted by the referral platform. See docs/REFERRAL_LEADS.md in the repository.

POST /api/referral-leads

Record a referral lead

Requires a signature with the platform or partner secret.

A person who clicked an instructor's AlertRoster offer on a pewpros surface and agreed to be contacted. Signed like AlertRoster's own callbacks (X-AlertRoster-Signature: t=<unix>,v1=<hmac-sha256 hex of "<t>.<raw body>">, five minutes either way), with X-AlertRoster-Key: platform (the shared platform secret) or partner (that partner's own secret). Idempotent on the partner and the email: a repeat answers 200 with the lead it already holds and re-sends nothing. A code naming no active partner answers 200 {"status":"ignored"} rather than a refusal, so the sender's retries are never stranded. Nothing here pays anything; rewards stay purchase-only. docs/REFERRAL_LEADS.md.

Request

curl -X POST "https://your-account.alertroster.com/api/referral-leads" \
  -H "X-AlertRoster-Signature: t=UNIX_SECONDS,v1=HMAC_SHA256_HEX" \
  -H "Content-Type: application/json" \
  -d '{
  "source": "community_post",
  "sender": {
    "host": "detroitarms.com",
    "site_id": "9a12…",
    "submission_id": 123
  },
  "email": "member@example.com",
  "phone": "+17025550142",
  "first_name": "Casey",
  "last_name": "Rivera",
  "referral_code": "SMITH-CCW",
  "consent_at": "2026-09-19T20:11:03Z",
  "consent_text": "By clicking, you agree that your name, email, and phone number will be shared with AlertRoster…",
  "resource_title": "Try AlertRoster",
  "sms_opt_in": false
}'

Response 200

{
  "status": "recorded",
  "lead_id": "5d0e…"
}

Lane B: the incidents API

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

GET /api/v2/incidents

Look up an incident by dedup key

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

For a caller that raised its event through Lane A, whose 202 carries an alert id and never an incident id. The source's open incident for dedup_key, otherwise its most recent one, so a decision that resolved it is still readable; 404 when the source has none. A lookup, not a list: one incident, on the credential's source (an API token names it with source_id). docs/LANE_B_API.md §5a.

Request

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

Response 200

{
  "incident": {
    "id": "4c1f…",
    "status": "resolved",
    "dedup_key": "cb-herd:alertroster#575"
  }
}
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. priority (normal, high or emergency) and sound (one of chime, critical, high, mail, normal, ping) are optional and decided at trigger: absent, the source's Default priority and Sound answer, then the urgency and the priority's own tone. The source's Loudest priority caps the priority; an unknown sound plays the source's tone. docs/LANE_B_API.md §4.

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 '{
  "priority": "high",
  "title": "Payment gateway timeouts",
  "dedup_key": "INC-4471",
  "urgency": "high",
  "sound": "chime"
}'

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"
  }
}