# Health / Status API

> Two open endpoints for monitors and dashboards: a single-purpose health probe and a structured per-component status snapshot.

Both endpoints are **open** (no API key required), **cached** for ~15 s at the
edge plus 30 s in-process so a flood of probes never burns through to the
underlying checks, and **safe to hit from any host** (no rate-limit attribution).

## `GET /api/verify`: minimal health

Use when all you need is "is links.et alive". Always returns 200 with a
tiny JSON body.

```bash
curl https://links.et/api/verify
```

```json
{ "ok": true, "ts": "2026-06-11T19:23:11.482Z" }
```

Don't use this to check whether an **upstream** (telebirr, CBE, …) is healthy;
this endpoint only reports on `links.et` itself.

## `GET /api/status`: structured per-component status

Use when you want to know which upstream is degraded or what response times
look like right now.

```bash
curl https://links.et/api/status
```

### Response

| HTTP | When |
| ----- | ----------------------------------------------------------------------------- |
| `200` | Overall status is `operational` or `slow` |
| `503` | Overall status is `down` (at least one component returned a 5xx or timed out) |

```json
{
	"ok": true,
	"status": "operational",
	"checkedAt": "2026-06-11T19:23:11.482Z",
	"cached": true,
	"components": [
		{
			"name": "verify-web (this service)",
			"host": "links.et",
			"group": "internal",
			"status": "operational",
			"responseMs": 0,
			"httpStatus": 200,
			"error": null
		},
		{
			"name": "Postgres (auth + API keys)",
			"host": "127.0.0.1:5432",
			"group": "internal",
			"status": "operational",
			"responseMs": 35,
			"httpStatus": null,
			"error": null
		},
		{
			"name": "Telebirr — transactioninfo.ethiotelecom.et",
			"host": "transactioninfo.ethiotelecom.et",
			"group": "upstream",
			"status": "operational",
			"responseMs": 522,
			"httpStatus": 200,
			"error": null
		}
	]
}
```

### Field reference

| Field | Type | Notes |
| ------------------------- | -------------- | ------------------------------------------------------------------------------- |
| `ok` | boolean | `true` unless overall status is `down` |
| `status` | enum | `operational` \| `slow` \| `down`; worst across all components |
| `checkedAt` | ISO timestamp | When the underlying probe round was last taken |
| `cached` | boolean | `true` if the snapshot came from the in-process 30 s cache |
| `components[].name` | string | Human label (e.g. `"Telebirr — transactioninfo.ethiotelecom.et"`) |
| `components[].host` | string | Upstream host or internal address |
| `components[].group` | enum | `internal` (the links.et service itself) or `upstream` |
| `components[].status` | enum | Per-component `operational` / `slow` / `down` |
| `components[].responseMs` | number \| null | Probe round-trip time |
| `components[].httpStatus` | number \| null | HTTP status the upstream returned (or `null` for non-HTTP probes like Postgres) |
| `components[].error` | string \| null | Network or timeout error message when the probe failed |

### Classification rules (same as the human-readable /status page)

- `down`: probe failed or upstream returned HTTP `>= 500`
- `slow`: probe succeeded but took longer than the per-component slow threshold (typically 1.5 s)
- `operational`: probe succeeded under threshold

## `HEAD /api/status`: uptime-monitor friendly

Same status code semantics as `GET /api/status`, no body. Cheapest possible
probe; recommended for monitors that only need to react to `200` vs `503`.

```bash
curl -I https://links.et/api/status
# HTTP/2 200
# cache-control: public, max-age=15, s-maxage=15
```

## Polling cadence

The in-process snapshot refreshes every 30 s and the public response is
cache-controlled for 15 s. A polling interval **between 30 s and 5 minutes**
is plenty; anything faster gets you the same cached snapshot, just with
extra bandwidth.
