# ingress-api Specification ## Purpose Read access to ingress definitions: the ingresses list and its summary numbers, every query backed by an index. ## Requirements ### Requirement: Ingresses list endpoint Edition: CE. `GET /api/v1/ingresses` SHALL list the ingress definitions of the activity range with the common query contract (`from`, `to`, `tz`, `limit` 1-100 default 25, opaque `cursor`), the filters `status`, `inputType` and `q`, the ranked sort keys `createdAt` (default), `totalDuration`, `sessions` and `lastSession` (`order` default `desc`), and the list envelope `{ items, nextCursor, prevCursor, total? }` with `total` under the `DASHBOARD_LIST_TOTAL_MAX_RANGE` rule. #### Scenario: Default list - **GIVEN** 30 ingresses created in the last 24 hours and 4 older ones that neither started a session in that range nor are publishing - **WHEN** the client requests `GET /api/v1/ingresses` with a valid token - **THEN** the response is `200` with the 25 most recently created ingresses of the last 24 hours, `nextCursor` set, `prevCursor: null` and `total: 30` #### Scenario: Cursor walk - **GIVEN** 60 ingresses in range and `limit=25` - **WHEN** the client follows `nextCursor` twice and then `prevCursor` once - **THEN** the pages hold ingresses 1-25, 26-50, 51-60 and 26-50 with no gaps or duplicates, each page ordered by `createdAt desc` with `_id desc` as tie-breaker #### Scenario: Ranked cursor walk - **GIVEN** 60 ingresses in the activity range, two of them publishing, and `sort=totalDuration&limit=25` - **WHEN** the client follows `nextCursor` twice - **THEN** the pages hold ingresses 1-25, 26-50 and 51-60 of the ranking at the first page's `asOf`, with no gaps or duplicates ### Requirement: Ingress list item Edition: CE. Each item of the ingresses list SHALL be the public projection of the ingress document: identity, definition facts, lifecycle and the aggregate session counters. `schemaVersion`, `expiresAt`, `participantName`, `url` and any stream key SHALL never be part of a list item. #### Scenario: Fields of an item - **WHEN** an item is read - **THEN** it has `ingressId`, `name?`, `inputType` (`rtmp` | `whip` | `url`), `roomName`, `participantIdentity`, `status` (`inactive` | `buffering` | `publishing` | `error` | `complete` | `deleted`), `enabled`, `reusable`, `createdAt`, `deletedAt?`, `sessions`, `totalDurationMs`, `lastSessionAt?` and `hasError`, and no `url`, `streamKey`, `participantName`, `schemaVersion` or `expiresAt` #### Scenario: Deleted ingress within retention - **GIVEN** an ingress deleted yesterday whose document has not expired - **WHEN** it is returned - **THEN** `status` is `deleted` and `deletedAt` is set #### Scenario: Error flag - **GIVEN** a reusable ingress whose last session ended in error and which is now `inactive` - **WHEN** it is returned - **THEN** `status` is `inactive` and `hasError` is `true` ### Requirement: Ingress status filter Edition: CE. The ingresses list SHALL accept `status` as one of `inactive`, `buffering`, `publishing`, `error`, `complete`, `deleted` or `all` (default `all`) and reject any other value with `400` `VALIDATION_ERROR`. #### Scenario: Publishing only - **WHEN** the client sends `?status=publishing` - **THEN** every item has `status: "publishing"` and the activity range still applies #### Scenario: Unknown status - **WHEN** the client sends `?status=active` - **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["status"]` #### Scenario: Old ingress publishing now - **GIVEN** an ingress created 30 days ago that is publishing now - **WHEN** the client sends `?status=publishing&from=now-1h&to=now` - **THEN** the ingress is in `items` ### Requirement: Input type filter Edition: CE. The ingresses list SHALL accept `inputType` as exactly one of `rtmp`, `whip` or `url`, SHALL return every input type when the parameter is absent, and SHALL reject any other value with `400` `VALIDATION_ERROR`. #### Scenario: WHIP only - **GIVEN** an RTMP, a WHIP and a URL ingress created in range - **WHEN** the client sends `?inputType=whip` - **THEN** `items` holds only the WHIP ingress #### Scenario: Unknown input type - **WHEN** the client sends `?inputType=srt` - **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["inputType"]` ### Requirement: Time range and sort key Edition: CE. The ingresses list SHALL apply `from`/`to` as an activity range, the union of the ingresses created in it (`createdAt_id`), those with `lastSessionAt` in it (`lastSessionAt_id`) and those publishing now (`status: publishing`) with `createdAt ≤ to` (`status_createdAt`). It SHALL accept `sort` only as `createdAt`, `totalDuration`, `sessions` or `lastSession` (else `400` `VALIDATION_ERROR`) and `order` as `asc` or `desc` (default `desc`). #### Scenario: Oldest first - **WHEN** the client sends `?order=asc` - **THEN** items are ordered by `createdAt asc` with `_id asc` as tie-breaker #### Scenario: Unsupported sort key - **WHEN** the client sends `?sort=lastSessionAt` - **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["sort"]` #### Scenario: Used in the range - **GIVEN** an inactive ingress created 20 days ago whose last session started 2 hours ago - **WHEN** the client sends `?from=now-24h&to=now` - **THEN** the ingress is in `items` #### Scenario: Publishing for days - **GIVEN** an ingress created 30 days ago whose session started 3 days ago and is publishing now - **WHEN** the client sends `?from=now-1h&to=now` - **THEN** the ingress is in `items` #### Scenario: Idle in the range - **GIVEN** an inactive ingress created 30 days ago whose last session started 10 days ago - **WHEN** the client sends `?from=now-24h&to=now` - **THEN** the ingress is not in `items` #### Scenario: Created after the range - **GIVEN** an ingress created today that is publishing now - **WHEN** the client sends a range that ended yesterday - **THEN** the ingress is not in `items` #### Scenario: Listed once - **GIVEN** an ingress created 1 hour ago whose only session started 50 minutes ago and is publishing now - **WHEN** the client sends `?from=now-24h&to=now` - **THEN** the ingress appears once in `items` and counts once in `total` ### Requirement: Name prefix search and id lookup Edition: CE. The ingresses list SHALL treat `q` as a case-insensitive prefix over the ingress name when it has at least two characters, combined with the activity range, `status` and `inputType`, SHALL reject a one-character `q` with `400`, and SHALL treat a value matching `^IN_[A-Za-z0-9]+$` as an exact id lookup that ignores `from`, `to`, `status`, `inputType` and `sort` and returns at most one item. #### Scenario: Prefix search - **GIVEN** ingresses named `Cam-1`, `cam-2` and `my-cam` created in range - **WHEN** the client sends `?q=CA` - **THEN** `items` holds `Cam-1` and `cam-2` ordered by `createdAt desc`, and not `my-cam` #### Scenario: One character - **WHEN** the client sends `?q=c` - **THEN** the response is `400` with code `VALIDATION_ERROR`, `details.issues[0].path` equal to `["q"]` and a message stating the two-character minimum #### Scenario: Exact id outside the range - **GIVEN** a deleted ingress `IN_ab12cd` created 40 days ago - **WHEN** the client sends `?q=IN_ab12cd&from=now-24h&status=publishing` - **THEN** the response is `200` with that single ingress in `items`, `total: 1` and both cursors `null` #### Scenario: Prefix of an ingress used in the range - **GIVEN** an ingress named `Studio` created 20 days ago whose last session started 1 hour ago - **WHEN** the client sends `?q=stu&from=now-24h&to=now` - **THEN** the ingress is in `items` ### Requirement: Ingresses summary endpoint Edition: CE. `GET /api/v1/ingresses/summary` SHALL accept `from`, `to` and `tz` and answer `200` with `{ publishingNow, ingressesInRange, byInputType, sessionsInRange, totalDurationMs, approximate }`, where `ingressesInRange` and `byInputType` count the ingresses the list selects for the range, the two session numbers are hour-aligned sums of the global hourly rollups and `approximate` is `true` when the range bounds are not on hour boundaries. #### Scenario: Numbers of a range - **GIVEN** 2 ingresses publishing now and created in the last 24 hours, 3 more created in the last 24 hours (5 in all: 3 RTMP, 2 WHIP), 12 sessions started and 6 hours of sessions ended in the hourly buckets of that range - **WHEN** the client requests `GET /api/v1/ingresses/summary?from=now-24h&to=now` - **THEN** the response is `{ publishingNow: 2, ingressesInRange: 5, byInputType: { rtmp: 3, whip: 2, url: 0 }, sessionsInRange: 12, totalDurationMs: 21600000, approximate: true }` #### Scenario: Hour-aligned range - **WHEN** the client requests `?from=2026-10-13T00:00:00Z&to=2026-10-13T12:00:00Z` - **THEN** `sessionsInRange` and `totalDurationMs` are the sums of `ingress.sessionsStarted` and `ingress.durationMs` of the twelve global hourly buckets and `approximate` is `false` #### Scenario: Publishing count ignores the range - **GIVEN** an ingress created 30 days ago that is publishing now - **WHEN** the client requests the summary with `from=now-1h` - **THEN** `publishingNow` counts it and `ingressesInRange` counts it too, as the list shows it #### Scenario: Reused ingress counted by activity - **GIVEN** an idle ingress created 30 days ago whose last session started 2 hours ago - **WHEN** the client requests the summary with `from=now-24h` - **THEN** `ingressesInRange` counts it and `publishingNow` does not #### Scenario: Ingresses selected for the range - **WHEN** the client requests the summary of a range - **THEN** `ingressesInRange` and `byInputType` count the ingresses created in the range, those with a session started in it and those publishing now and created before `to` ### Requirement: Index-backed ingress queries Edition: CE and PRO. Every query of this capability SHALL run on an index of the shared contract (`dashboard-ce/typings/contract/indexes.ts`), or on `_id` for the hourly rollups the long-range summary reads: no winning plan contains a `COLLSCAN`, and `totalDocsExamined` is bounded by the page size plus the documented residual filters, and for the list by the ingresses of the three branches of the activity range. #### Scenario: Explain plans on the generated database - **GIVEN** a database generated by the data generator at scenario M - **WHEN** the list (every filter combination and sort) and summary queries are explained - **THEN** each winning plan uses an index of the shared contract and none contains `COLLSCAN` #### Scenario: Activity range over 30 days - **GIVEN** a database generated at scenario M for 30 days - **WHEN** the list is explained with each sort key over 30 days for `createdAt` and over 7 days for the others - **THEN** the three branches use `createdAt_id`, `lastSessionAt_id` and `status_createdAt`, `totalDocsExamined` is at most the ingresses the branches select, and the p95 of a page is within 200 ms ### Requirement: Ranked ingress sorts Edition: CE. Every ingress sort SHALL rank in memory as `api-conventions` defines: `createdAt` ranks `createdAt` over any range; `totalDuration` ranks `totalDurationMs` plus `asOf − lastSessionAt` while publishing, `sessions` ranks `sessions` and `lastSession` ranks `lastSessionAt`, these three accepted when the range is within `DASHBOARD_LIST_TOTAL_MAX_RANGE` and answered `400` with code `SORT_RANGE_TOO_WIDE` beyond it. #### Scenario: Total duration with a running session - **GIVEN** in the activity range: an inactive ingress with `totalDurationMs` of 3 hours, an inactive ingress with 20 minutes, and an ingress publishing with `totalDurationMs` of 1 hour whose last session started 90 minutes before `asOf` - **WHEN** the client sends `GET /api/v1/ingresses?sort=totalDuration` - **THEN** `items` holds the 3-hour ingress, the publishing ingress (2 h 30 min at `asOf`) and the 20-minute ingress, in that order #### Scenario: Most used first - **GIVEN** ingresses in the activity range with `sessions` 12, 1 and 5 - **WHEN** the client sends `?sort=sessions&order=desc` - **THEN** `items` holds the ingresses with 12, 5 and 1 sessions, in that order #### Scenario: Latest session first - **GIVEN** ingresses in the activity range whose last sessions started 2 hours, 10 minutes and 6 hours ago - **WHEN** the client sends `?sort=lastSession` - **THEN** `items` holds them in the order 10 minutes, 2 hours and 6 hours ago #### Scenario: Creation order over any range - **WHEN** the client sends `?sort=createdAt&from=now-90d&to=now` - **THEN** the response is `200` with the ingresses of the activity range newest first #### Scenario: Metric sort beyond the limit - **GIVEN** the default `DASHBOARD_LIST_TOTAL_MAX_RANGE` of 7 days - **WHEN** the client sends `?sort=sessions&from=now-30d&to=now` - **THEN** the response is `400` with code `SORT_RANGE_TOO_WIDE`