> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build on-call alerting with the On-Call API

> Model a rotation, build an escalation policy, and drive incidents from open to resolved with the On-Call API, then page responders via Messages.

# Build on-call alerting with the On-Call API

If you run infrastructure, you eventually need to answer three questions when something breaks: *who is on call right now*, *who gets paged next if they don't respond*, and *has anyone acknowledged this yet*. The [On-Call API](/api-reference/oncall) gives you those three answers as pure, stateless compute — you send a rotation or policy definition, Orbit resolves it — without locking you into a separate incident-alerting vendor or making you learn a second messaging API to actually reach someone.

This guide walks through the full loop: define a rotation, build an escalation policy on top of it, drive an incident through its lifecycle, and send the pages it produces.

<Note>
  On-Call is compute-only. It never pages anyone and never persists state on Orbit's side — you own the incident record (in your own database, or even in memory for a quick script) and pass its current snapshot on every call. The one thing it does *not* do for you is the send itself: that goes through the [Messages API](/api-reference/endpoints/messaging), same as any other Orbit send.
</Note>

## Step 1 — Define a rotation

A rotation is an ordered list of members and a cadence. `anchor` is the ISO instant the first member's shift begins; every hand-off after that is one shift length later, so the math stays correct across DST changes.

```json theme={null}
{
  "name": "Platform primary",
  "cadence": "weekly",
  "anchor": "2026-08-03T09:00:00Z",
  "members": ["usr_1", "usr_2", "usr_3"]
}
```

`cadence` is `daily`, `weekly`, or `custom`. For `custom`, also set `shift_length_seconds` (for example `43200` for 12-hour shifts). Members are opaque strings — Orbit never interprets them, so use whatever id you already page with (a user id, an E.164 number, an email address).

### Resolve who's on call

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/oncall/resolve \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "rotation": {
      "name": "Platform primary",
      "cadence": "weekly",
      "anchor": "2026-08-03T09:00:00Z",
      "members": ["usr_1", "usr_2", "usr_3"]
    }
  }'
```

The response returns `on_call` (the current member), the `shift_start` / `shift_end` window, and `next_on_call` / `next_handoff_at`. Pass an optional `"at"` (ISO instant) to resolve who was — or will be — on call at a different time, which is useful for testing a rotation before it goes live.

## Step 2 — Build an escalation policy

A policy is an ordered list of steps. Each step pages a **target** — either a live rotation (resolved at page time, so a hand-off mid-incident still pages the right person) or a fixed list of users — over one or more channels, and waits `escalate_after_seconds` for an acknowledgement before moving to the next step.

```json theme={null}
{
  "name": "Sev-1 platform",
  "repeat": 1,
  "steps": [
    {
      "target": { "type": "rotation", "rotation": { "name": "Platform primary", "cadence": "weekly", "anchor": "2026-08-03T09:00:00Z", "members": ["usr_1", "usr_2", "usr_3"] } },
      "channels": ["push", "sms"],
      "escalate_after_seconds": 300
    },
    {
      "target": { "type": "users", "members": ["usr_9"] },
      "channels": ["voice"],
      "escalate_after_seconds": 600
    }
  ]
}
```

`repeat` re-runs the whole policy that many additional times if nobody ever acknowledges — set it to `0` for a policy that pages once through and stops.

### Plan the escalation timeline

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/oncall/escalation/plan \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "policy": { "name": "Sev-1 platform", "repeat": 1, "steps": [ ... ] } }'
```

The response is a flattened, absolute-offset timeline: for every page, which `targets` and `channels`, at what `offset_seconds` from incident start, and the concrete `fire_at` instant. This is the schedule you hand to Step 3.

## Step 3 — Drive the incident lifecycle

You own the incident record. On-Call gives you two stateless operations against a snapshot you keep in your own store:

* **`POST /oncall/incident/tick`** — "what's due right now?" Pass the incident snapshot (its policy, `started_at`, current `status`, and `pages_fired` high-water mark) and Orbit returns the pages due this tick, the new high-water mark to persist, and `next_tick_at` — when to call `tick` again. Call this on a timer (or lazily, whenever your own scheduler wakes up) until the incident is acknowledged, resolved, or `exhausted` (every page fired with no response).
* **`POST /oncall/incident/transition`** — apply an operator action: `ack`, `resolve`, or `reassign` (swap in a new policy and restart the timeline, e.g. escalating a Sev-2 to a Sev-1 policy). An invalid transition — acknowledging an already-resolved incident, for example — is rejected with `409 incident_transition_invalid` instead of silently corrupting your state.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/oncall/incident/tick \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "incident": {
      "id": "inc_123",
      "policy": { "name": "Sev-1 platform", "repeat": 1, "steps": [ ... ] },
      "started_at": "2026-08-08T14:02:00Z",
      "status": "open",
      "pages_fired": 0
    }
  }'
```

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/oncall/incident/transition \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "incident": { "id": "inc_123", "policy": { ... }, "started_at": "2026-08-08T14:02:00Z", "status": "open", "pages_fired": 1 },
    "action": { "type": "ack", "by": "usr_2" }
  }'
```

Persist the returned incident snapshot after every `tick` or `transition` call — that snapshot (specifically its `pages_fired` mark and `status`) is the only state On-Call asks you to keep.

## Step 4 — Page the resolved targets

`tick` tells you *which* targets and channels are due — it does not send anything. Fan each due page out through the [Messages API](/api-reference/endpoints/messaging) on the channel(s) the step named:

```bash theme={null}
# For each due page from /incident/tick, send on its channel(s):
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "to": "usr_2", "body": "[Sev-1] Platform incident inc_123 — ack at https://status.example.com/inc_123" }'
```

Subscribe to [delivery-status webhooks](/webhooks/overview) so you know whether a page actually reached its target before the `escalate_after_seconds` window elapses.

## Frequently asked questions

### Does On-Call replace PagerDuty or Opsgenie?

Not necessarily — it's the same rotation/escalation primitives those tools use, built on your existing Orbit channels. Some teams use it standalone; others use it to build a lightweight alerting path for one service without paying for a full incident-management seat.

### Does Orbit store my rotations or incidents?

No. Every On-Call endpoint is pure compute over the request body — you send the full rotation, policy, or incident snapshot on every call, and Orbit returns the resolved answer. Persistence is entirely on your side.

### What happens if nobody acknowledges the incident?

Once every page across every step and repeat round has fired with no `ack`, `tick` reports `status: "exhausted"` and `next_tick_at: null` — there is nothing left to schedule. Your driver should treat this as its own alert (e.g. page a fallback channel or open a ticket).

### Can I change who gets paged mid-incident?

Yes — send a `reassign` transition with a new policy. This restarts the escalation timeline from the top under the new policy, which is useful for escalating a Sev-2 incident to a broader Sev-1 policy.

## See also

* [On-Call API reference](/api-reference/oncall) — full endpoint, scope, and field reference
* [Messages API](/api-reference/endpoints/messaging) — the send surface that actually pages a resolved target
* [Webhooks](/webhooks/overview) — subscribe to delivery status for a page you sent
