# Oncall API Catalog

Full endpoint reference for the oncall service. Read this when you need the exact
verb, path, auth level, parameters, or response fields for a call. The mental
model, auth setup, and workflow live in `../SKILL.md` — read that first.

Verified against `api/router.go`, `api/team_router.go`, `api/middleware.go`.
Keep this file in sync when those routes change.

Conventions used below:
- Host: production is `https://oncall.pingcap.net`, so a path like `/api/v1/me`
  is `https://oncall.pingcap.net/api/v1/me`. See SKILL.md → Base URL.
- `{ref}` — a team's numeric id **or** its unique slug name (e.g. `payments`).
- `{eref}` — an engineer id or email.
- `?` after a field means optional. `[]` means an array.
- Times are unix epoch seconds; schedule windows render in UTC+8.
- **auth** is the minimum role: `authenticated` < `team_writer` < `super_admin`
  (see SKILL.md → Roles).

## Contents
- [Identity](#identity)
- [Schedule reads](#schedule-reads)
- [Team reads](#team-reads)
- [Team writes](#team-writes)
- [Rotation layers](#rotation-layers)
- [Overlays (manual overrides)](#overlays-manual-overrides)
- [Notifications](#notifications)
- [API keys](#api-keys)

---

## Identity

### `GET /api/v1/me` — who am I
- **auth:** authenticated
- **returns:** `email`, `name`, `managed_team_ids`, `read_team_ids`, `is_super_admin`, `can_create_team`
- Good first call: tells you which teams you can write to before you try.

---

## Schedule reads

### `GET /api/v1/schedule/overview` — current on-call snapshot, all teams
- **auth:** authenticated
- **query:** `group_name?` (filter to one group)
- **returns:** array of `{team_id, team_name, display_name, group_names, shifts, backup}`
- The "who's on call right now" board across every team.

### `GET /api/v1/schedule` — resolved schedule over a window
- **auth:** authenticated
- **query:** `team_ids` (repeatable or CSV, **required**), `start_time?`, `end_time?`, `is_backup?`, `include_empty?`
- **returns:** `team_ids`, `start_time`, `end_time`, `is_backup`, `rows`
- `rows[]` carry `engineer_email`, `start_time`/`end_time`, `overlay`, `lane`,
  `source_type`. This is the **effective** schedule (baseline + overlays) — the
  real view to trust, not the pure layer projection.

### `GET /api/v1/system-phones` — system caller-ID reference
- **auth:** authenticated
- **returns:** `groups`, `feishu_notice`
- Static, read-only, same for everyone: the system's caller-ID phone numbers
  (grouped by provider/region) plus a Feishu urgent-call notice.

---

## Team reads

### `GET /api/v1/teams` — list teams
- **auth:** authenticated
- **query:** `group_name?`
- **returns:** `teams[]` of `{team_id, team_key, display_name, group_names}`

### `GET /api/v1/teams/{ref}` — team detail
- **auth:** authenticated
- **returns:** `team` (full object), `availables` (current members, each with `is_manager`), `is_manager`, `can_write`
- `is_manager`/`can_write` describe **the caller** — trust them instead of
  inferring write permission yourself.

### `GET /api/v1/teams/{ref}/current-oncall` — who is on call now
- **auth:** authenticated
- **query:** `at_time?` (default: now)
- **returns:** `team_id`, `at_time`, `current_engineer_emails`, `backup_engineer_emails`, `current_no_oncall`, `next_switch_at`
- The call for paging / alert integrations.

### `GET /api/v1/teams/{ref}/schedule/computed` — pure layer projection
- **auth:** authenticated
- **query:** `from?`, `to?` (default `from`+14d)
- **returns:** `team_id`, `from`, `to`, `segments`
- No overlays applied. For the schedule users actually see, use
  `GET /api/v1/schedule` instead.

### `GET /api/v1/teams/{ref}/layers` — rotation layers
- **auth:** authenticated
- **returns:** `team_id`, `backup_enabled`, `layers`
- `layers[]` = `{id, priority, people, rotate_every_seconds, on_call_at_once,
  combine_mode, source_team_id, active_mode, active_days, active_start_min,
  active_end_min, next_handoff_at, next_up_email}`.

### `GET /api/v1/teams/{ref}/available-engineers` — current members
- **auth:** authenticated
- **returns:** `team_id`, `engineers[]` of `{engineer_id, engineer_email, engineer_name, is_manager}`
- These are the team's **current** members (the path name is historical), not a
  pool of people you can add.

### `GET /api/v1/teams/{ref}/managers` — list managers
- **auth:** authenticated
- **returns:** `team_id`, `managers[]` of `{engineer_id, engineer_email, engineer_name}`

---

## Team writes

### `POST /api/v1/teams` — create team
- **auth:** super_admin
- **body:** `name` (immutable slug), `display_name?`, `managers[]?`, `group_names[]?`, `sort_priority?`, `description?`
- **returns:** `status`

### `PATCH /api/v1/teams/{ref}` — update team
- **auth:** team_writer
- **body:** `display_name?`, `group_names[]?`, `sort_priority?`, `managers[]?` (replaces the whole set), `description?`
- **returns:** `status`
- `name` (the slug) is immutable.

### `DELETE /api/v1/teams/{ref}` — disable team
- **auth:** team_writer
- **returns:** `status`
- Soft-delete. Only when explicitly requested.

### `POST /api/v1/teams/{ref}/engineers` — add member
- **auth:** team_writer
- **body:** `engineer_email?` (preferred) or `engineer_id?`
- **returns:** `status`

### `DELETE /api/v1/teams/{ref}/engineers/{eref}` — remove member
- **auth:** team_writer
- **returns:** `status`

### `POST /api/v1/teams/{ref}/managers` — add manager
- **auth:** team_writer
- **body:** `engineer_email?` or `engineer_id?`
- **returns:** `status`

### `DELETE /api/v1/teams/{ref}/managers/{email}` — remove manager
- **auth:** team_writer
- **returns:** `status`
- Don't remove a team's last manager.

---

## Rotation layers

Layers define the recurring rotation. A team's effective baseline is built by
stacking layers by `priority`. Each layer's on-call comes from **either** its own
rotating `people`, **or** another team (a reference layer). Never set rotation
phase by writing shift rows — use `handoff_at`/`next_up_email` on the layer.

### `POST /api/v1/teams/{ref}/layers/preview` — dry-run layer change
- **auth:** team_writer
- **body:** `layers[]` (same payload as the replace call below)
- **returns:** `handoff_at`, `current_engineer_emails`, `next_engineer_emails`, `segments`, `warnings`
- Writes nothing. Always call this before `PUT .../layers`.

### `PUT /api/v1/teams/{ref}/layers` — replace the layer set
- **auth:** team_writer
- **body:** `layers[]`, each:
  - `id` (keep the existing id for an in-place update; omit to create)
  - `priority`, `on_call_at_once`
  - `combine_mode`: `override` = when this layer is active it is the only one on
    call; `append` = also keep the lower-priority layers on call
  - `active_mode`, `active_days`, `active_start_min`, `active_end_min` (when the
    layer is active, e.g. business hours only)
  - `handoff_at?`, `next_up_email?` (sets rotation phase)
  - **source — exactly one of:**
    - own people: `people[]` + `rotate_every_seconds`, **or**
    - reference layer: `source_team_id` (on-call follows another team; no
      `people`/`rotation`)
- **returns:** `team_id`, `backup_enabled`, `layers`
- Replaces the **whole** layer set, so send every layer you want to keep. Preview first.

### `PUT /api/v1/teams/{ref}/backup-lane` — toggle backup lane
- **auth:** team_writer
- **body:** `enabled`
- **returns:** `team_id`, `backup_enabled`, `layers`

---

## Overlays (manual overrides)

Overlays sit on top of the baseline; a higher `overlay` value wins. This is the
main write primitive for "just change who covers this specific range."

### `POST /api/v1/teams/{ref}/schedule/preview` — dry-run a segment override
- **auth:** team_writer
- **body:** `start_time`, `end_time`, `engineer_email?` (empty = no on-call), `is_backup?`
- **returns:** `dry_run`, `team_id`, `start_time`, `end_time`, `new_email`, `new_overlay`, `is_backup`, `effective_before`
- Call before `apply_overlay` with a `segment`.

### `POST /api/v1/overlays` — apply an overlay
- **auth:** team_writer (for every team referenced)
- **body:**
  - `type`: `segment` | `items`
  - `segment` → `{team_id, start_time, end_time, is_backup?, engineer_email?` **or** `items[].engineer_email}`
  - `items` → `items[]` of `{team_id, engineer_email|engineer_id, start_time, end_time, is_backup?, overlay?, overlay_mode? (fixed|max_plus_one), reason?}`
- **returns:** `status`, `type`, `items`
- `segment` sets who covers a range (an **empty** assignee list is a deliberate
  "no on-call"). `items` is for advanced multi-team writes.

### `DELETE /api/v1/overlays` — remove overlays in a range
- **auth:** team_writer
- **query:** `team_id` (**required**), `start_time?`, `end_time?`, `is_backup?`
- **returns:** `status`, `removed_count`, `is_backup`
- Reverts the range to the baseline schedule.

---

## Notifications

These have **external side effects** — confirm with the user before sending, and
prefer the matching `preview` first.

### `POST /api/v1/notifications/feishu/preview` — preview handover message
- **auth:** authenticated
- **body:** `group_name`, `at_time?`, `skip_same_substitute?`, `exclude_test_teams?`
- **returns:** `group_name`, `items`, `message_text`

### `POST /api/v1/notifications/feishu` — send handover message
- **auth:** authenticated
- **body:** `group_name`, `at_time?`, `skip_same_substitute?`, `exclude_test_teams?`, `recipients[]?`, `recipient_emails[]?`
- **returns:** `status`, `preview`
- Sends the Feishu handover message.

### `POST /api/v1/notifications/private/preview` — preview privilege-grant batch
- **auth:** super_admin
- **body:** `group_name`, `at_time?`, `exclude_test_teams?`
- **returns:** `group_name`, `batches`

### `POST /api/v1/notifications/private` — send privilege-grant batch
- **auth:** super_admin
- **body:** `group_name`, `at_time?`, `exclude_test_teams?`
- **returns:** `preview`, `results`
- Executes the privilege-grant batch.

---

## API keys

### `GET /api/v1/api-keys` — list keys
- **auth:** authenticated
- **returns:** `keys[]` of `{id, key_prefix, name, type, scopes, created_at}`
- The secret is never returned, only `key_prefix`.

### `POST /api/v1/api-keys` — create a user key
- **auth:** authenticated
- **body:** `name`
- **returns:** `id`, `name`, `key`, `key_prefix`
- A **user** key inherits the caller's permissions. The plaintext `key` is
  returned **once** — capture it now.

### `POST /api/v1/api-keys/service` — create a service key
- **auth:** super_admin
- **body:** `name`, `scopes[]` (subset of `[oncall:read, oncall:write]`; `oncall:read` is always included)
- **returns:** `id`, `name`, `type`, `scopes`, `key`, `key_prefix`
- A least-privilege machine key. Plaintext `key` returned once.

### `DELETE /api/v1/api-keys/{id}` — delete a key
- **auth:** authenticated (own keys only)
- **returns:** `status`
