---
title: "Read passive platform signals"
method: GET
path: "/computers/{id}/signals"
tags: ["Platform signals"]
---

# Read passive platform signals

`GET /computers/{id}/signals`

## Request

- Base URL: `https://app.mandala.computer/api/v1`
- URL: `https://app.mandala.computer/api/v1/computers/{id}/signals`
- Auth: HTTP bearer

Member-only, finite reads of daemon facts. No guest connection, wake, watcher, output read or Activity capture. An absent or empty since starts at the current head with baseline:true and no history. Replay uses the full opaque cursor; it advances past filtered rows. An expired, malformed, restarted or migrated epoch returns an explicit gap with a new head. These are ephemeral observations, not durable history, complete process coverage or task-success proof. Pages contain at most 100 events and 65536 encoded bytes. Query strings are at most 4096 characters; unknown or duplicate parameters are refused. Both emission scope and current daemon scope must match. Requests have finite routing/body deadlines and may fail unavailable; failures never become empty history. Responses are no-store.

Requires the `member` role or higher.

## Path parameters

- `id` string, required

## Query parameters

- `since` string
- `limit` integer

## Response `200`

A safe baseline, finite replay page or explicit reset gap.

- PlatformSignalPage
  - `computer` string, required
  - `from` string, required: Opaque start checkpoint, or current head on baseline/reset.
  - `cursor` string, required: Next opaque checkpoint, including filtered-out ring rows.
  - `events` PlatformSignal[], required
    - union
      - object
        - `seq` integer, required
        - `cursor` string, required: Opaque checkpoint after this event. Never join executions by PID.
        - `at` string, date-time, required
        - `computer` string, required
        - `source` 'daemon', required
        - `type` 'process.exited', required
        - `data` union, required
          - object
            - `pid` integer, required
            - `execution_id` string: Optional stable execution identity; never inferred from PID.
            - `exit_code` integer, required
          - object
            - `pid` integer, required
            - `execution_id` string: Optional stable execution identity; never inferred from PID.
            - `lost` true, required
      - object
        - `seq` integer, required
        - `cursor` string, required: Opaque checkpoint after this event. Never join executions by PID.
        - `at` string, date-time, required
        - `computer` string, required
        - `source` 'daemon', required
        - `type` 'computer.started', required
        - `data` object, required
          - `status` 'running', required
          - `previous` 'running' | 'stopped' | 'suspended' | 'building' | 'build-failed' | 'half-removed': Absent for a first observation.
      - object
        - `seq` integer, required
        - `cursor` string, required: Opaque checkpoint after this event. Never join executions by PID.
        - `at` string, date-time, required
        - `computer` string, required
        - `source` 'daemon', required
        - `type` 'computer.stopped', required
        - `data` object, required
          - `status` 'stopped', required
          - `previous` 'running' | 'stopped' | 'suspended' | 'building' | 'build-failed' | 'half-removed': Absent for a first observation.
      - object
        - `seq` integer, required
        - `cursor` string, required: Opaque checkpoint after this event. Never join executions by PID.
        - `at` string, date-time, required
        - `computer` string, required
        - `source` 'daemon', required
        - `type` 'computer.suspended', required
        - `data` object, required
          - `status` 'suspended', required
          - `previous` 'running' | 'stopped' | 'suspended' | 'building' | 'build-failed' | 'half-removed': Absent for a first observation.
      - object
        - `seq` integer, required
        - `cursor` string, required: Opaque checkpoint after this event. Never join executions by PID.
        - `at` string, date-time, required
        - `computer` string, required
        - `source` 'daemon', required
        - `type` 'computer.idle', required
        - `data` object, required
          - `idle_seconds` integer, required: A daemon idle-policy judgment, not proof that no work is running.
  - `more` boolean, required: Another eligible event is owed; poll with cursor.
  - `baseline` boolean, required: The first read starts at current head with no historical replay.
  - `gap` PlatformSignalGap
    - `cursor` string, required
    - `at` string, date-time, required
    - `type` 'gap', required
    - `computer` string, required
    - `source` 'daemon', required
    - `data` object, required
      - `oldest_cursor` string
      - `detail` 'events happened that this computer can no longer replay; anything you were waiting on may already have occurred', required
  - `supported` string[], required
  - `retention` 'ephemeral', required

## Other responses

- `400`: The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.
- `401`: Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).
- `403`: The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: "revoked"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.
- `404`: No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest. Complete scoped discovery proves the computer missing or outside scope.
- `405`: This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.
- `429`: You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: "contention"` (see `POST /snapshots/{id}/clone`). The ordinary rate limit or bounded in-flight capacity is exhausted. Capacity refusals include Retry-After.
- `500`: The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.
- `501`: This feature is not available: no hypervisor in the fleet can build a template, or the platform or the host holding this computer does not offer it. Unlike a `409`, this does not clear on its own — retrying will not help, so treat it as an outage of the feature rather than of the moment. A scoped host still holds this computer but lacks signals. Body code is signals_unsupported and reason is unsupported. No active observer fallback.
- `503`: Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead. Host discovery, scope, protocol validation or the bounded transport is unavailable. Body code is signals_unavailable; keep the prior checkpoint.

---

[API](https://skmtc.dev/mandala/apis/mandala-computer-api.md) · [All operations](https://skmtc.dev/mandala/apis/mandala-computer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mandala/apis/mandala-computer-api/revisions/2afbb87fa703?raw)
