Dashboard and admin API
The dashboard shows the server’s log and state in a browser; the admin API behind it is what tests and scripts use to switch scenarios, add stubs and read the log.
Dashboard
http://127.0.0.1:4010/__apigenie/ui is a page served by the server itself, for a browser on the
same machine: it follows the admin API’s rules (local clients only unless
admin.allow_remote, on admin.port when the admin API has a port of its own, off with
--admin off). The header on every screen names the documents and how they answer (mock, proxy,
generated), where they listen, the seed, the uptime, the requests and violations so far, and has
Reload and Stop; with several services a switcher narrows every screen to
one.
| Screen | What it shows and does |
|---|---|
| Traffic | Every exchange as it happens (the event stream), with filters; an exchange’s request and answer (headers and bodies, captured while the page is open) and its validation problems with where they point in the document. Copy as curl, Replay, Save as stub (POST /stubs), Add to .http (a file built up in the browser, to copy or download), Copy as YAML (the answer as an operations entry of the configuration). Record session and Save session: a session recording of what happens from here, written where the server runs. |
| Operations | The operations by tag, with their hits, their last status and what answered them last (an example, a generated body, a scenario, a proxy). For one operation: a fixed answer (a declared status, one of its examples, a generated body, a dropped connection), a latency and whether its requests are validated, saved as an always-on override (PUT /overrides) until the server stops, with the Prefer header (and __ parameters) that do the same for one request. |
| Scenarios and network | The scenarios of the configuration, the document and the admin API, switched on and off, with their rules and hits; a builder for a new one (by operation, tag, method and path, header or query parameter; status, example, latency, probability); the network simulation of every response (fixed, uniform, normal or log-normal latency, jitter, dropped connections, bandwidth, the seed) with the response times of the last requests. |
| Contract | Whether the server would pass --fail-on and --min-coverage if it stopped now, the coverage, the violations by check (and the exchanges they were seen in), the operations never called, the report as JSON or JUnit to download, and Start over (DELETE /contract). |
| Data | The document store’s collections with their documents, Reset to seed, snapshots to save and restore; and ctx.state, the values the scripts keep, to edit or reset. |
| Channels | For an AsyncAPI document only (the screen is listed when a served document has channels): the channels by WebSocket, MQTT and HTTP, with their operations, the clients connected and the messages counted (GET /asyncapi/channels); for a channel, its messages in and out as the event stream brings them (with an exchange’s payload and validation problems, and Open in Traffic), its clients (GET /asyncapi/clients, followed through the ws-open, ws-close, mqtt-subscribe and mqtt-unsubscribe events), and Publish: a message sent as the application would (POST /asyncapi/publish), to every client of the channel or to one, with a payload started from the document’s examples or left to the server; a payload the message’s schema refuses shows the violations and goes nowhere, until Send anyway; Copy as curl. |
Every panel copies its settings as the configuration’s YAML, or as x-apigenie-mock for the
document, so what was tried in the dashboard is kept in a file (Channels changes no setting: it
copies the publish as curl). The page follows the system’s light or dark theme, or the one chosen
in the header.
The page is built from the web components package (src/app_web, the apg-server-*
elements) and embedded in the binary; nothing is fetched from the network. The design is ADR-167.

Admin API
On by default for local clients only (admin.allow_remote opens it, admin.port moves it to a
port of its own). The /db endpoints work on the namespace of an X-Apigenie-Test header (or a
?test= parameter), the default one without it:
| Endpoint | Description |
|---|---|
GET /__apigenie |
Status: version, url (where clients reach the server), document (with its proxies and stubs; an AsyncAPI document’s mqttProxy and mqttClients), seed, uptime (uptimeSeconds, startedAt), requests (the clients’, not the admin API’s), active scenarios, simulation, logSize. |
GET /__apigenie/ui/ |
The dashboard: the page and its files (GET /__apigenie/ui redirects to it). |
GET /__apigenie/routes |
The operations served, with their hits, lastStatus, lastSource (what answered last) and lastAt; an OpenAPI operation’s tags, deprecated and responses (each status with its content types and the names of its examples). |
DELETE /__apigenie/hits |
Start the hits of routes and scenarios over (a reload keeps them). |
GET /__apigenie/services |
The services: name, mount, URL, document (see several services). |
GET /__apigenie/scenarios, PUT / DELETE /__apigenie/scenarios/{name} |
List (with hits, lastStatus, lastSource and lastAt, and the rules as ruleList next to their count rules), add or remove scenarios. |
POST /__apigenie/scenarios/{name}/activate, /deactivate |
Switch a scenario on or off. |
GET, PUT /__apigenie/overrides |
The always-on rules, whole: what GET answers, PUT takes back. |
GET, PUT /__apigenie/simulation |
The network simulation of every response. |
POST /__apigenie/seed |
Reseed: {"seed": 42}. |
GET, DELETE /__apigenie/log?limit=&after=&level= |
The recent events, with headers and bodies when they were captured (see log entries). |
GET /__apigenie/events?type=&level=&service=&after= |
The events as they happen, as Server-Sent Events (see the event stream). |
POST /__apigenie/stubs |
A stub file of a logged exchange: {"id": 42, "dir": "stubs", "name": "rex"} (see stubs from the log). |
GET /__apigenie/record |
The session recording: recording, and the session’s dir, name, started, exchanges while one is on; test, the mark. |
POST /__apigenie/record/start, /stop |
Record a session in {"dir": "sessions/run-7", "name": "run 7"} (dir defaults to record.dir), or stop it and write session.yaml. |
POST /__apigenie/record/mark |
{"test": "checkout-happy"}: the test of the exchanges to come that carry no X-Apigenie-Test of their own; {} clears it. |
GET /__apigenie/replay |
The session replay’s report so far: exchanges, replayed, neverReplayed, unmatched (with their tests and closest recordings), passed, and tests, the exchanges each test used since its reset. |
POST /__apigenie/replay/reset |
Restart a test’s counters: {"test": "checkout"}, or the X-Apigenie-Test header, or ?test=; without one, every test’s. 204. |
POST /__apigenie/reload, POST /__apigenie/shutdown |
Reload, or stop gracefully. |
GET /__apigenie/spec |
The served document, bundled (converted to OpenAPI 3.2 for a Swagger 2.0 document; a .proto file with its imports, or a GraphQL schema, as text). |
GET, PUT, DELETE /__apigenie/state/{key} |
Values kept between requests. |
GET /__apigenie/db |
The document store: its file, namespaces, snapshots and collections (documents, seeds, id, and the schema and operations of a resource’s collection). |
GET /__apigenie/db/{name}?limit=&offset= |
A collection’s documents (100 by default) and total. |
PUT, DELETE /__apigenie/db/{name} |
Replace every document of a collection (a JSON array; all or nothing: 422 when the schema refuses one, 409 for an id used twice), or remove them. |
POST /__apigenie/db/reset |
Back to the seeds: with X-Apigenie-Test, that namespace; without it, the default one, and the tests’ namespaces go. {"collections": ["notes"]} resets some. |
GET /__apigenie/db/snapshots, PUT, DELETE /__apigenie/db/snapshots/{name} |
List, take (or replace) or remove a snapshot of the documents. |
POST /__apigenie/db/snapshots/{name}/restore |
Put a snapshot’s documents back. |
POST /__apigenie/db/save |
Write the store to --db-file now (501 without one). |
DELETE /__apigenie/db/namespaces/{test} |
Forget a test’s namespace. |
GET, DELETE /__apigenie/contract |
The contract report (coverage and violations), or start it over. |
GET /__apigenie/contract.json, GET /__apigenie/contract.xml |
The contract report as --report (JSON) and --report-junit (JUnit XML) would write it now, to save. |
GET /__apigenie/contract/violations?operation=&check=&limit= |
The violations seen, by operation (id or GET /pets/{id}; GraphQL: the operation’s name or a root field such as Query.book; AsyncAPI: the operation’s key; gRPC: package.Service/Method or its path) and check. |
POST /__apigenie/contract/schema-check |
GraphQL proxy: introspect the server and compare its schema now (202; the result is logged and reported). |
GET /__apigenie/asyncapi/channels |
AsyncAPI: the channels, their operations and messages (with the document’s examples), the clients connected, and the messages counted (see AsyncAPI channels). |
GET /__apigenie/asyncapi/clients |
AsyncAPI: the WebSocket clients and MQTT subscriptions of the channels. |
POST /__apigenie/asyncapi/publish |
AsyncAPI: send a message to a channel’s clients, or to one of them: {"channel": "rooms/42", "payload": {"text": "hi"}}, {"channel": "room", "client": 7, ...}. |
GET /__apigenie/grpc/services |
gRPC: the services and their methods. |
GET, POST, DELETE /__apigenie/grpc/stubs |
gRPC: list, add (one stub or a list) or remove the stubs added at run time (?scope=all: every stub). |
GET, PUT, DELETE /__apigenie/grpc/stubs/{name} |
gRPC: one stub. |
POST /__apigenie/grpc/stubs/reset, GET /__apigenie/grpc/stubs/schema |
gRPC: back to the stub files, and the stubs’ JSON Schema. |
GET, DELETE /__apigenie/grpc/history?method=&limit=&after=&q= |
gRPC: the calls received, newest last. |
POST /__apigenie/grpc/verify |
gRPC: count the calls that match {method, request, metadata, stub, code}. |
A server of several services names the service of each item, and takes
?service=NAME on every endpoint.
The event stream
GET /__apigenie/events keeps the connection open and writes each event of the log as it happens,
as Server-Sent Events: id: is
the log entry’s id, event: its kind, and data: the same JSON object as a /log entry. A
dashboard reads it with EventSource; a script with curl -N:
curl -N 'http://127.0.0.1:4010/__apigenie/events?type=request,violation'
: connected
id: 12
event: request
data: {"event":"request","id":12,"time":"2026-10-12T09:00:00.000Z","level":"info","method":"POST","path":"/pets","operationId":"addPet","status":201,...}
id: 13
event: request
data: {"event":"request","id":13,...,"status":422,"violations":[...]}
id: 13
event: violation
data: {"event":"request","id":13,...,"status":422,"violations":[...]}
| Event | When |
|---|---|
request |
An exchange, request and answer together (there is no separate response event). |
violation |
After an event with violations: the same id and data. |
ws-open, ws-in, ws-out, ws-close, mqtt-*, grpc, grpc-in, grpc-out |
WebSocket, MQTT and gRPC traffic, as in the log. |
scenario |
A scenario was added, replaced, removed, activated or deactivated through the admin API (message says which). |
reload |
The documents were reloaded (POST /reload or --watch). |
contract-updated |
The contract report’s coverage changed (at most once a second), or it started over (message: "reset", after DELETE /contract or a reload). |
db |
A write to the document store: the collection, what changed, who wrote. |
admin, script, server |
The admin API’s own exchanges, the scripts’ logs, the server’s errors. |
?type= keeps the named events, ?level= those at a level and above (default debug),
?service= one service’s. ?after=ID, or the Last-Event-ID header a reconnecting EventSource
sends, first replays the log’s later entries, so a client that reconnects misses nothing the log
still holds. A comment every 15 seconds keeps the connection alive. A client that falls more than
1 MiB behind is disconnected. The stream follows the admin API’s rules: local clients only unless
admin.allow_remote, on admin.port when the admin API has a port of its own.
Log entries
While at least one event stream is open, the server captures the headers and bodies of every
exchange, as log.headers: true and log.bodies: true do. /log entries and stream events
then carry headers and responseHeaders (arrays of [name, value]), requestBody and
responseBody, and captured: {"headers": true, "bodies": true}. A body that isn’t UTF-8 is in
Base64 with requestBodyEncoding (or responseBodyEncoding) "base64". A body is kept up to
log.body_limit bytes (4096 by default); a longer one is cut and marked requestBodyTruncated or
responseBodyTruncated, and requestSize and responseSize keep the whole sizes.
Stubs from the log
POST /__apigenie/stubs makes a stub file of a logged exchange of an OpenAPI document,
as --record-dir writes them: the client’s headers without the connection’s, the hidden headers
of proxy.redact as [redacted], and the query without the mock’s own __ parameters.
curl -X POST http://127.0.0.1:4010/__apigenie/stubs -d '{"id": 12, "dir": "stubs", "name": "new-pet"}'
With dir, or else the service’s proxy.record_dir, the stub is written to a new file there,
<operationId>-N.yaml, and the answer (201) has the file and the stub. Without either the
answer (200) is the file’s content, {"stubs": [...]}. The entry must still be in the log, and
must have been captured whole (an open event stream, or log.headers and log.bodies, and a
log.body_limit above its bodies’ sizes): else 404 or 409. The running mock answers with the new
stub after POST /__apigenie/reload when dir is one of its stubs directories.
AsyncAPI channels from the admin API
GET /__apigenie/asyncapi/channels lists the channels: key, address, protocols, paths,
messages (their names) and messageList (each message’s name, contentType, summary and
examples, the document’s, each with its payload and headers: a starting point for a payload
to publish), operations (with qos and retain for MQTT), the clients connected now and the
contract report’s connections, messagesIn, messagesOut and unmatched counts.
GET /__apigenie/asyncapi/clients lists the WebSocket clients and MQTT subscriptions: id,
protocol, remote, channel, address, route, path (the WebSocket path, or the MQTT
filter), clientId (MQTT), connectedAt and messagesOut.
POST /__apigenie/asyncapi/publish sends a message to the clients of a channel, as the schedule
sends one: scripts see it, and each delivery is logged as ws-out or mqtt-out with
source: "admin".
curl -X POST http://127.0.0.1:4010/__apigenie/asyncapi/publish \
-d '{"channel": "rooms/42", "message": "notice", "payload": {"text": "maintenance at noon"}}'
{"channel": "room", "address": "rooms/42", "operation": "sendNotices", "message": "notice", "source": "admin",
"contentType": "application/json", "payload": "{\"text\":\"maintenance at noon\"}", "delivered": 1,
"webSocket": 1, "mqtt": 0, "clients": [7]}
With a Kafka tap, a Kafka channel’s message is produced to
the cluster instead. The record’s key, headers (an object of strings) and partition may be
given; without them the key and headers are made from the message’s Kafka binding and headers, and
the partition is the key’s. The record is checked as the tap checks records (422 with the
violations, nothing produced, unless "validate": false), produced, and answered with where it
went; a cluster that can’t be reached or refuses it answers 502. The tap then reads it like any
other record.
curl -X POST http://127.0.0.1:4010/__apigenie/asyncapi/publish \
-d '{"channel": "orders", "payload": {"sku": "A-1"}, "key": "cust-1", "headers": {"correlationId": "c-1"}}'
{"channel": "orders", "operation": "placeOrder", "message": "order", "source": "admin",
"contentType": "application/json", "payload": "{\"sku\":\"A-1\"}",
"kafka": {"upstream": "kafka://localhost:9092", "topic": "shop.orders", "partition": 1, "offset": 42,
"key": "cust-1", "headers": {"correlationId": "c-1"}}}
With an AMQP tap, an AMQP channel’s message is published to
the broker: a queue channel’s through the default exchange, any other to its exchange with
routingKey (else the address given, else the address with its parameters filled). Its
properties (contentType, contentEncoding, deliveryMode, priority, correlationId,
replyTo, expiration, messageId, timestamp, type, userId, appId) and headers may be
given; without them they come from the message’s and the operation’s AMQP bindings and the message’s
headers. The message is checked (422), published mandatory with publisher confirms, and answered
with {"amqp": {"upstream", "exchange", "routingKey", "headers"}}; a message no queue takes answers
409 asyncapi-unroutable.
channel is a channel’s key (every client of the channel gets the message) or an address with its
parameters (that address’s clients only); client, an id of /asyncapi/clients, narrows it to
that one client. message names one of the channel’s messages; without it, the first message of
its first send operation is sent. payload is the message as JSON, raw as text, and without
either one is made as for the schedule. A payload that doesn’t match the message’s schema answers
422 with the violations and is sent to nobody, unless "validate": false. Unknown channels and
messages answer 404, as does a client that isn’t connected to the channel
(asyncapi-unknown-client). An admin publish isn’t retained by the MQTT
broker. Only clients the mock matched to a channel get it. Clients of a validating proxy are counted
in the status, not listed. A server of several services lists every AsyncAPI service’s channels and
clients, and needs ?service= to publish when several services serve AsyncAPI.