# Admin API

> The routes of the gateway's admin port - probes, configuration in force, simulation, journals, metrics - each with a call and its answer.

Reference - Since version 0.1.0 - Flexway v1.7.1 - https://flexway.io/en/docs/reference-admin-api/

The gateway serves its admin surface on a **port of its own**, `:9090` by default
(`FLEXWAY_ADMIN_ADDR`), never on the traffic port. The full contract of this surface is published with
the documentation, as OpenAPI 3.1 (`admin.openapi.yaml`); this page shows how to use it.

> [!CAUTION]
> This port must **never** be routable from outside: `/config` lists the issuers and the upstreams,
> `/errors` every tenant's errors, `/simulate` describes every decision. Open it to the orchestrator,
> the metrics scraper and the console only. In Kubernetes, the chart gives it a `ClusterIP` `Service`
> of its own and a `NetworkPolicy` enabled by default.

| Route | Role | Since |
| --- | --- | --- |
| [`GET /healthz`](#get-healthz) | The process is alive | `0.1.0` |
| [`GET /readyz`](#get-readyz) | Ready to serve | `0.1.0` |
| [`GET /config`](#get-config) | Configuration in force, secrets masked | `0.1.0` |
| [`GET /metrics`](#get-metrics) | Metrics in the Prometheus format | `0.2.0` |
| [`POST /simulate`](#post-simulate) | The decision on a described request, without the upstream | `1.0.0` |
| [`POST /simulate/mqtt`](#post-simulatemqtt) | The MQTT listener's decision on a described packet, without the broker | `1.7.0` |
| [`GET /events`](#get-events) | The replica's journal of events | `1.2.0` |
| [`GET /errors`](#get-errors) | The replica's journal of recent errors | `1.3.0` |
| [`GET /sparkplug/nodes`](#get-sparkplugnodes) | The replica's table of Sparkplug B nodes | `1.7.0` |

## GET /healthz

The process is alive - never a statement about its configuration: a process alive but not ready must be
left alone, not restarted in a loop. Always `200`.

```bash
curl -s http://localhost:9090/healthz
# {"status":"ok","uptime_sec":42}
```

## GET /readyz

A configuration is loaded and served. `200` with the number of operations; `503` with the reason before
the first load and from the start of a shutdown (the replica leaves the rotation before it drains).

```bash
curl -s http://localhost:9090/readyz
# 200 {"status":"ready","operations":3}
# 503 {"status":"not ready","reason":"no snapshot loaded yet"}
```

## GET /config

Every key of `flexway.yaml` as loaded, secret values masked, plus what the gateway derives from it:
`build` (version and commit), `config_version`, `config_digest`, `last_reload`, `catalog` (the operations
served), `chains` (each operation's policy chain and its provenance) and, when configured, `agents`,
`kafka`, `ai`, `a2a`, `mcp_federation`. `503 not_ready` while no configuration is loaded.

```bash
curl -s http://localhost:9090/config | jq '{build, config_digest, last_reload}'
```

## GET /metrics

The Prometheus exposition of the gateway's metrics (`text/plain`). Their names and dimensions are a
stable surface since `1.0.0`: see [Observing](https://flexway.io/en/docs/operations-observability/).

```bash
curl -s http://localhost:9090/metrics | grep '^flexway_'
```

## POST /simulate

What the gateway would do with a **described** request: routing, origin, authorization against the
described identity (no token is verified), whitelist, validation, policy chain - then a stop before the
upstream: no token exchanged, no bucket nor quota spent, nothing counted. `method`, `host` and `path` are
required; `headers` and `identity` (`issuer`, `subject`, `tenant`, `scopes`, `roles`) optional.

```bash
curl -s http://localhost:9090/simulate -d '{"method":"GET","host":"api.example.com","path":"/orders?status=open",
  "identity":{"issuer":"customers","subject":"alice","tenant":"acme","scopes":["orders:read"]}}'
```

`200` with `verdict` (`served` or `refused`), `status`, a refusal's `code`, `operation`, `upstream`,
`chain`, the policies applied and the outbound request when served; `400 malformed_request` when the
description is incomplete.

## POST /simulate/mqtt

What the MQTT device listener would do with a **described** packet (`publish`, `subscribe` or `will`, a
`CONNECT`'s will) of a described device (`identity`: `kind` `token`, `api_key` or `certificate`, `id` of a
key or of an `auth.mtls` client, `issuer`, `subject`, `tenant`, `scopes`, `claims`, certificate fields):
the operations the identity satisfies, channels with their bound values written, wildcards contained,
`$share/` groups, QoS, Response Topic - then it stops before the broker: nothing dialled, no rate nor
quota spent, nothing counted. `packet`, `topic` and `identity` are required. `404` without an MQTT
listener.

```bash
curl -s http://localhost:9090/simulate/mqtt -d '{"packet":"publish","topic":"plant/acme/press-07/telemetry","qos":1,
  "identity":{"kind":"api_key","id":"press-07"}}'
```

`200` with `verdict` (`served` or `refused`), a refusal's `reason_code` and `cause`, `operation`,
`upstream`, the topic or filter `written` toward the broker, the `qos` it sees and the bound `client_id`;
`400 malformed_request` when the description is incomplete.

## GET /events

What happened on **the replica that answers**: reloads (`reload`), circuit breaker transitions
(`breaker`), shedding (`shedding`), refused agent chains (`delegation`). The last 128 events of each kind,
newest first, in memory - lost at a restart. Never a tenant nor anything a caller sent.

```bash
curl -s http://localhost:9090/events | jq '.events[0]'
# {"at":"...","kind":"reload","result":"loaded","version":"7fbb3dc21e1b"}
```

## GET /errors

The recent errors of the replica that answers, in three rings of 256 (`upstream`, `refused`, `internal`),
newest first, at most 20 writes per second and per kind (past that, an error is counted in its kind's
`dropped`). Each entry carries the operation, the upstream, the verified tenant, the status, the code,
the stage, the cause, the duration and the trace when sampled; never the path received, the headers or
the body.

```bash
curl -s http://localhost:9090/errors | jq '.errors[0]'
```

## GET /sparkplug/nodes

The table of the Sparkplug B edge nodes and host applications that cross the answering replica: state (`online`,
`offline`, `moved`, `unknown`), `bdSeq`, gaps, device counts (`devices_online`, `devices_total`), commands admitted
and refused; `?tenant=` filters one tenant, `?tenant=&node=` answers that node alone with its devices listed. The
listing never carries devices: at most `max_nodes` entries of bounded fields (an id is 255 bytes at most).
Bounded by `observability.sparkplug`, lost at a restart; `404` without an MQTT listener. Node names exist only there,
never in a metric ([Sparkplug B and OPC UA PubSub JSON](https://flexway.io/en/docs/operations-sparkplug/#the-node-table)).

```bash
curl -s 'http://localhost:9090/sparkplug/nodes?tenant=acme' | jq '.nodes[0].state'
```

## Unknown route

Any other route answers `404` in `application/problem+json` (RFC 9457), like every error of this
surface:

```json
{
  "type": "https://flexway.dev/problems/not_found",
  "title": "No operation is declared for this route",
  "status": 404,
  "detail": "No such admin route.",
  "instance": "/debug/pprof/",
  "code": "not_found"
}
```
