# External API

The external API gives read access to your fleet from outside the dashboard. Use it for a status page, a customer portal, or your own scripts. Every endpoint is a `GET` under `/api/external/` and authenticates with an API key in the `x-api-key` header.

## API keys

Create a key under Settings, API Keys. The full key is shown once, when you create it. Copy it then. The list afterwards shows each key's name, scope and when it was last used. Revoke a key from the same list.

Keys created from the dashboard have the `read` permission. A key acts as the user who created it. It sees what that user can see, never more.

```sh
curl -H "x-api-key: $DAEMONITOR_API_KEY" https://daemonitor.com/api/external/fleet
```

A missing, unknown or expired key returns `401`. A key without the `read` permission returns `403`. Each accepted request updates the key's last-used time.

### Organization-scoped keys

A key can be limited to one [organization](./sharing.md#organizations). An org-scoped key returns only systems assigned to that organization, on every endpoint. Asking for another system by id returns `404`, and `/groups` lists only groups with a member in the organization, with the other members left out. The scope only narrows. The key still resolves through its user's own access first, so it can't reach a system the user couldn't. Revoking the user's access revokes the key's.

This is the key to hand a customer-facing tool. An unscoped key would show that customer every other customer's systems you own.

The dashboard doesn't offer the scope yet. Create a scoped key with a signed-in request:

```
POST /api/me/api-keys/create
{ "name": "customer portal", "orgId": "<organization id>" }
```

You must be a member of the organization.

## Which endpoints see shared systems

Endpoints fall into two groups.

| Visibility | Endpoints |
|---|---|
| Systems the key's user owns | `/systems`, `/systems/:systemId` |
| Everything the user can see, including shares and organization membership | `/fleet`, `/fleet/:systemId`, `/systems/:systemId/history`, `/groups`, `/incidents` |

For a system the user received through a group share, the second group returns only the services in that group.

`/fleet`, `/fleet/:systemId`, `/systems` and `/systems/:systemId` leave out archived systems and archived services.

## GET /api/external/fleet

The whole visible fleet, with each system's services normalised into one shape. Use this rather than `/systems` unless you need the raw plugin data. The normalised shape stays stable when a plugin's own payload changes.

```json
{
  "version": 1,
  "generatedAt": "2026-10-14T09:00:00.000Z",
  "systems": [
    {
      "id": "3f0c...",
      "name": "web-01",
      "status": "online",
      "last_seen": "2026-10-14T08:59:41.000Z",
      "services": [ /* Service[] */ ]
    }
  ]
}
```

Each system object carries the system's own fields plus `services`. The system's ingest key is never included, on this or any other endpoint.

### The Service object

The main fields:

| Field | Type | Meaning |
|---|---|---|
| `id` | string | The service id. |
| `name` | string | Display name. |
| `stack` | string | The id of the system it runs on. |
| `kindLabel`, `badge` | string | What kind of service it is, for display. |
| `level` | `ok`, `warn`, `crit`, `standby` | Current level. `crit` also covers a service that stopped reporting. |
| `statusWord` | string | The one-line status, such as `OFFLINE · 12m` or a reason from the server. |
| `statusReason` | string | The server's reason, when there is one. |
| `cpu`, `mem` | number | Percentages, where the service has them. |
| `ping` | number | Latency in ms, where the service has it. |
| `age` | number | Seconds since the last report. |
| `units` | array | Sub-items, such as containers in a compose project. |
| `metrics` | array | Extra stat tiles. |
| `sparkline`, `load` | number[] | Recent samples for small charts. |
| `plugins` | string[] | Plugins behind this service. |
| `lastReport` | string | Time of the last report, preformatted. |
| `isGroup`, `isCluster` | boolean | Set on rollup items. |

A host that is correctly not running a clustered service has `level: "standby"`. The cluster rollup card itself is not included. See [Clusters](./clusters.md#limits).

The full type is `Service` in `@daemonitor/common`, `types/fleet.ts`.

## GET /api/external/fleet/:systemId

One visible system, normalised the same way. Returns `404` for a system the key can't see, so a caller can't probe for other people's system ids.

```json
{
  "version": 1,
  "generatedAt": "2026-10-14T09:00:00.000Z",
  "system": { "id": "3f0c...", "name": "web-01" },
  "services": [ /* Service[] */ ]
}
```

The system and its services are siblings here. In `/fleet` the services sit inside each system.

## GET /api/external/systems

Systems owned by the key's user, each with its raw service rows. Shared systems are not included.

Returns an array:

```json
[
  {
    "id": "3f0c...",
    "name": "web-01",
    "status": "warning",
    "status_reason": "Memory usage is high",
    "states": [
      {
        "id": "a81b...",
        "unique_id": "os",
        "type": "os",
        "status": "warning",
        "status_reason": "Memory usage is high",
        "data": { },
        "updated_at": "2026-10-14T08:59:41.000Z"
      }
    ]
  }
]
```

`states` holds one row per service, newest report first. `data` is the plugin's last report exactly as the agent sent it, so its shape depends on the plugin.

## GET /api/external/systems/:systemId

One owned system with its `states`, in the same shape as one element of `/systems`. Returns `404` for a system the user doesn't own or that is archived.

## GET /api/external/systems/:systemId/history

A short time series for one service.

| Query | Meaning |
|---|---|
| `state` | Required. The service id, the `id` of a row in `states`. |
| `limit` | Number of points returned. Default 60, maximum 240. |

```json
{
  "points": [12, 14, 13],
  "label": "CPU",
  "unit": "%",
  "cpu": [12, 14, 13],
  "mem": [61, 61, 62]
}
```

`points` is the primary series for the service type. For `os` it is CPU in percent and `mem` carries memory in percent. For `web` it is response time in ms. For Cloudflare projects it is request counts. For containers and pm2 it is CPU. The arrays run oldest to newest.

A `state` the key can't see returns empty arrays rather than an error. A system it can't see returns `404`.

## GET /api/external/groups

Groups visible to the key's user. That includes their own, those of any account delegated to them, and groups shared with them directly. Sorted by `sort`.

```json
[
  {
    "id": "9c2d...",
    "name": "Storefront",
    "description": "Public shop and its API",
    "color": "#2f81f7",
    "sort": 0,
    "user_id": "5e1a...",
    "kind": "website",
    "cf_zone": "example.com",
    "members": ["a81b...", "c03f..."]
  }
]
```

`members` lists service ids, the same ids `/fleet` returns. A member id outside the services your `/fleet` call returned resolves to nothing.

`kind` is free text set in the dashboard, or `null`. `cf_zone` is the Cloudflare zone linked to the group, or `null`.

## GET /api/external/incidents

[Incidents](./incidents.md) marked client-visible that name at least one system the key can see. Returns at most 200, newest start first.

| Query | Meaning |
|---|---|
| `since` | ISO timestamp. Only incidents updated at or after it. Lets a poller fetch just what changed. |
| `open` | Any value. Only incidents with no end yet. |

```json
{
  "version": 1,
  "generatedAt": "2026-10-14T09:00:00.000Z",
  "incidents": [
    {
      "id": "7d41...",
      "title": "Checkout errors",
      "severity": "crit",
      "status": "monitoring",
      "startedAt": "2026-10-14T02:10:00.000Z",
      "endedAt": null,
      "summary": "Payment API timed out under load.",
      "solution": null,
      "updatedAt": "2026-10-14T03:05:00.000Z",
      "targets": [
        { "type": "system", "id": "3f0c...", "label": "web-01", "systemId": "3f0c..." }
      ],
      "updates": [
        { "id": "e9a0...", "kind": "status", "body": "Fix deployed, watching error rate.", "url": null, "status": "monitoring", "at": "2026-10-14T03:00:00.000Z" }
      ]
    }
  ]
}
```

`targets` leaves out systems the key can't see. `updates` contains only client-visible timeline entries, oldest first, and never says who wrote them.
