openapi: 3.2.0
info:
  title: vlatte admin API
  version: "1"
  summary: Manage a vlatte mock server while it runs.
  description: |
    The admin API of a vlatte mock server. It is an HTTP server of its own, on its own address, the same for mock
    servers of every protocol: an operation the server's protocol can't do answers `501` with the code `not_supported`.

    **Formats.** Bodies are JSON or YAML: send `Content-Type: application/json`, the default, or `application/yaml`.
    Answers follow `Accept`: JSON by default, YAML when it is preferred. `Accept` that takes neither is `406`.

    **Errors.** Every error is an RFC 9457 problem document, `application/problem+json` or `application/problem+yaml`,
    with a stable `code`. Problems of a mock or a body list each in `problems`.

    **Safety.** The admin API asks for no credentials unless it is given some: then every route needs them, Basic or a
    bearer token, and a request without them gets `401` with the code `unauthorized`, but the Kubernetes probes,
    `/livez`, `/readyz` and `/startupz`, which answer their status alone. A browser sends the token as the password of
    Basic. Without credentials, it listens on a local address unless told otherwise. Browsers' cross-site requests
    that change state are refused with `403`.

    **Persistence.** A mock server with a persistent store, such as the files of the vlatte command or a database,
    writes each change this API makes to its mocks to that store at once: the mocks it adds outlive the server. The
    server serves what the store holds when it starts and on each reload, changes made to the store directly included.
    Without one, the mocks this API adds last as long as the server.
  license:
    name: MIT
    url: https://github.com/caffeine-projects/vlatte/blob/main/LICENSE
tags:
  - name: Server
    description: The admin API and the mock server it manages.
  - name: Mocks
    description: The mocks the server serves, from files, code and this API.
  - name: Scenarios
    description: |
      Scenarios: mocks that match in turn, as a scenario moves from state to state. A scenario starts in `started`.
      HTTP mock servers only.
  - name: KV
    description: |
      The key-value store, whose values templates read as `.KV`, such as `{{ .KV.Get "token" }}`. A value set here is
      read from the next request on. Values are text. In the vlatte command, its HTTP and gRPC servers share one store.
  - name: Journal
    description: |
      The journal of the requests the server served, of every outcome, newest first: a changelog in files, indexed,
      kept across restarts when the server names a directory. A mock server without a journal answers `501`.
  - name: Streams
    description: What the server serves and logs, as it happens, as Server-Sent Events.
  - name: Recording
    description: |
      Recording a service as mock files: the server sends the requests the recording takes to its target, and writes
      the answers as mock files, which it serves once the recording stops. HTTP mock servers only.
  - name: MCP
    description: The admin API's operations as tools of the Model Context Protocol, for AI agents.
  - name: Documentation
    description: This document, and a page that renders it.
security:
  - basic: []
  - bearer: []
paths:
  /health:
    get:
      tags: [Server]
      operationId: getHealth
      summary: Check the admin API
      description: Answers once the admin API serves, with the kind of the mock server and the URL of its mocks.
      responses:
        "200":
          description: The admin API serves.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
            application/yaml:
              schema: { $ref: "#/components/schemas/Health" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
  /livez:
    get:
      tags: [Server]
      operationId: getLivez
      summary: Probe liveness
      description: |
        The liveness probe of Kubernetes. Answers while the admin API serves, as `/health` does, whatever the mock
        store says, so a store that is down doesn't get the server restarted. It needs no credentials: when the admin
        API asks for some, it answers the status alone.
      security: []
      responses:
        "200":
          description: The admin API serves.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
            application/yaml:
              schema: { $ref: "#/components/schemas/Health" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
  /readyz:
    get:
      tags: [Server]
      operationId: getReadyz
      summary: Probe readiness
      description: |
        The readiness probe of Kubernetes. Answers as `/health` does once the mock store can serve its mocks, and `503`
        while it can't, such as when the database behind a persistent store is down. It needs no credentials: when the
        admin API asks for some, it answers the status alone, and a `503` that doesn't say why.
      security: []
      responses:
        "200":
          description: The mock store serves its mocks.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
            application/yaml:
              schema: { $ref: "#/components/schemas/Health" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "503": { $ref: "#/components/responses/NotReady" }
  /startupz:
    get:
      tags: [Server]
      operationId: getStartupz
      summary: Probe startup
      description: |
        The startup probe of Kubernetes. The admin API serves only once the mock server loaded its mocks, so this
        answers, as `/livez` does, once the mock server started. It needs no credentials.
      security: []
      responses:
        "200":
          description: The mock server started.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
            application/yaml:
              schema: { $ref: "#/components/schemas/Health" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
  /reset:
    post:
      tags: [Server]
      operationId: reset
      summary: Reset the server between tests
      description: |
        Removes the mocks added through this API, loads the mock files again, saved ones included, starts every
        scenario over, and clears the journal. Mocks added in code stay as they are. A server without mock files,
        scenarios, or a journal, skips them. A persistent store loads again too: its mocks come back as it holds them,
        and nothing is deleted from it. The key-value store stays as it is: `POST /kv/reload` loads its file again.
      responses:
        "204":
          description: The mocks, scenarios and journal are back to how they started.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/DuplicateID" }
        "500": { $ref: "#/components/responses/LoadFailed" }
  /mocks:
    get:
      tags: [Mocks]
      operationId: listMocks
      summary: List mocks
      description: Every mock, enabled or not, in the order mocks match requests.
      responses:
        "200":
          description: The mocks.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MockList" }
            application/yaml:
              schema: { $ref: "#/components/schemas/MockList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
    post:
      tags: [Mocks]
      operationId: addMock
      summary: Add a mock
      description: |
        Adds the mock of the body, a mock file. Without an `id`, the mock gets a new one. An `id` in use is a `409`:
        replace that mock with `PUT /mocks/{id}`. A relative `body_file` opens from the server's working directory.
      requestBody: { $ref: "#/components/requestBodies/Mock" }
      responses:
        "201":
          description: The mock was added.
          headers:
            Location:
              description: Where the mock is, such as `/mocks/hello`.
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MockSummary" }
            application/yaml:
              schema: { $ref: "#/components/schemas/MockSummary" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/DuplicateID" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
    delete:
      tags: [Mocks]
      operationId: removeAllMocks
      summary: Remove every mock
      description: |
        Removes every mock, of files and code too, and empties the persistent store. `POST /mocks/reload` loads the
        mock files again.
      responses:
        "204":
          description: The server has no mocks.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
  /mocks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The id of the mock.
        schema: { type: string, pattern: "^[A-Za-z0-9._~-]+$" }
    get:
      tags: [Mocks]
      operationId: getMock
      summary: Get a mock
      description: The mock as a mock file describes it, which `PUT` and `POST` take back.
      responses:
        "200":
          description: The mock.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Mock" }
            application/yaml:
              schema: { $ref: "#/components/schemas/Mock" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/MockNotFound" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "500": { $ref: "#/components/responses/DescribeFailed" }
        "501": { $ref: "#/components/responses/NotSupported" }
    put:
      tags: [Mocks]
      operationId: replaceMock
      summary: Replace a mock
      description: |
        Swaps the mock for the mock of the body in one step, so a request finds one or the other. The body's `id` must
        be the path's. A mock replaced this way gives way to a mock file's mock with its id on the next reload.
      requestBody: { $ref: "#/components/requestBodies/Mock" }
      responses:
        "200":
          description: The mock was replaced.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MockSummary" }
            application/yaml:
              schema: { $ref: "#/components/schemas/MockSummary" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "404": { $ref: "#/components/responses/MockNotFound" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
    patch:
      tags: [Mocks]
      operationId: updateMock
      summary: Enable or disable a mock
      description: A disabled mock matches no request until it is enabled again.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MockChange" }
          application/yaml:
            schema: { $ref: "#/components/schemas/MockChange" }
      responses:
        "200":
          description: The mock, changed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MockSummary" }
            application/yaml:
              schema: { $ref: "#/components/schemas/MockSummary" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "404": { $ref: "#/components/responses/MockNotFound" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
    delete:
      tags: [Mocks]
      operationId: removeMock
      summary: Remove a mock
      responses:
        "204":
          description: The mock was removed.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "404": { $ref: "#/components/responses/MockNotFound" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
  /mocks/validate:
    post:
      tags: [Mocks]
      operationId: validateMock
      summary: Check a mock without adding it
      description: Builds the mock of the body as `POST /mocks` would, and answers the same, without adding it.
      requestBody: { $ref: "#/components/requestBodies/Mock" }
      responses:
        "200":
          description: The mock would be added.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MockSummary" }
            application/yaml:
              schema: { $ref: "#/components/schemas/MockSummary" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/DuplicateID" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /schema:
    get:
      tags: [Mocks]
      operationId: getMockSchema
      summary: Get the mock file schema
      description: |
        The JSON Schema of the server's mock files, in JSON or YAML as `Accept` asks, so a client writes mocks the
        server takes. HTTP mock servers only.
      responses:
        "200":
          description: The JSON Schema of the mock files.
          content:
            application/json:
              schema: { type: object }
            application/yaml:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /mocks/reload:
    post:
      tags: [Mocks]
      operationId: reloadMocks
      summary: Load the mock files and the persistent store again
      description: |
        Swaps the mocks of the mock files, and of the persistent store, for what they hold now, in one step. A mock
        added through this API gives way to a mock file's mock with its id. When a file has problems, nothing changes.
      responses:
        "204":
          description: The mock files were loaded.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/DuplicateID" }
        "500": { $ref: "#/components/responses/LoadFailed" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /mocks/save:
    post:
      tags: [Mocks]
      operationId: saveMocks
      summary: Save the mocks added through this API as mock files
      description: |
        Writes each mock added through this API as a mock file, `<id>.mock.<format>`, into the directory, then serves
        it from its file, which loads from then on: on the next start too, when the server's mock file patterns take
        the directory, as they take `{workdir}/_mocks`, in the server's work directory, and YAML by default. Its relative body files are named from the
        directory. When a mock can't be described as a mock file, nothing is written.
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SaveSettings" }
          application/yaml:
            schema: { $ref: "#/components/schemas/SaveSettings" }
      responses:
        "200":
          description: The mock files written.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SavedFiles" }
            application/yaml:
              schema: { $ref: "#/components/schemas/SavedFiles" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/DuplicateID" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "500": { $ref: "#/components/responses/SaveFailed" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /scenarios:
    get:
      tags: [Scenarios]
      operationId: listScenarios
      summary: List scenarios
      description: >-
        Every scenario the mocks name, and every one set without a mock, in the order of their names: its state, the
        states its mocks name, and the moves they make. A scenario is in `started` until something moves it.
      responses:
        "200":
          description: The scenarios.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScenarioList" }
            application/yaml:
              schema: { $ref: "#/components/schemas/ScenarioList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
    delete:
      tags: [Scenarios]
      operationId: resetScenarios
      summary: Start every scenario over
      description: Moves every scenario back to `started`.
      responses:
        "204":
          description: Every scenario is in `started`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /scenarios/{name}:
    parameters:
      - name: name
        in: path
        required: true
        description: The name of the scenario, in any case, so Checkout is checkout.
        schema: { type: string }
    put:
      tags: [Scenarios]
      operationId: setScenario
      summary: Move a scenario to a state
      description: >-
        The mocks that require the state match next. A state none of the scenario's mocks names is an `unknown_state`
        problem; a scenario no mock names moves to any state. Without `key`, the shared instance moves.
      parameters:
        - name: key
          in: query
          description: The key of the instance to move, as the scenario key of its mocks renders it. Empty is the shared instance.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScenarioChange" }
          application/yaml:
            schema: { $ref: "#/components/schemas/ScenarioChange" }
      responses:
        "200":
          description: The scenario, moved.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Scenario" }
            application/yaml:
              schema: { $ref: "#/components/schemas/Scenario" }
        "400":
          description: "`invalid_body`: the body has problems. `unknown_state`: none of the scenario's mocks names the state."
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
            application/problem+yaml:
              schema: { $ref: "#/components/schemas/Problem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
    delete:
      tags: [Scenarios]
      operationId: resetScenario
      summary: Start a scenario over
      description: Moves every instance of the scenario back to `started`, or only the instance of `key`.
      parameters:
        - name: key
          in: query
          description: The key of the only instance to reset. Empty is the shared instance.
          schema: { type: string }
      responses:
        "204":
          description: The scenario is in `started`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /scenario-board:
    get:
      tags: [Scenarios]
      operationId: getScenarioBoard
      summary: Manage scenarios from a browser
      description: |
        A page that shows where each scenario is, the states its mocks name and which mock moves it where, moves a
        scenario to a state, and starts one or all over. It follows `GET /events` to show moves as they happen.
      responses:
        "200":
          description: The page.
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /kv:
    get:
      tags: [KV]
      operationId: listKV
      summary: List the keys
      description: The keys of the key-value store, in order, without their values. An empty store lists none.
      responses:
        "200":
          description: The keys.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KVKeys" }
            application/yaml:
              schema: { $ref: "#/components/schemas/KVKeys" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /kv/reload:
    post:
      tags: [KV]
      operationId: reloadKV
      summary: Load the key-value store again from its file
      description: |
        Replaces every value with those of the store's file, such as the CSV file of the vlatte command's `kv.file`:
        values set since the last load are gone, and keys removed since come back. The whole file is read and checked
        first, so a request reads the old values or the new ones, never a mix. `POST /mocks/reload` and `POST /reset`
        leave the store as it is.
      responses:
        "204":
          description: The store holds the file's values.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "500":
          description: "`load_failed`: the file can't be read, or has a problem, which `detail` names with its line. Nothing changed."
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
            application/problem+yaml:
              schema: { $ref: "#/components/schemas/Problem" }
        "501":
          description: "`not_supported`: the store loads from no file, such as one kept in memory alone."
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
            application/problem+yaml:
              schema: { $ref: "#/components/schemas/Problem" }
  /kv/{key}:
    parameters:
      - name: key
        in: path
        required: true
        description: The key, one path segment. A key with a slash is sent escaped, so `a%2Fb` is the key `a/b`.
        schema: { type: string }
    get:
      tags: [KV]
      operationId: getKV
      summary: Get the value of a key
      description: The bytes of a key or a value that aren't UTF-8 are replaced with U+FFFD.
      responses:
        "200":
          description: The key and its value.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KVEntry" }
            application/yaml:
              schema: { $ref: "#/components/schemas/KVEntry" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/KeyNotFound" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
    put:
      tags: [KV]
      operationId: setKV
      summary: Set a key to a value
      description: Templates read the value from the next request on. An empty value is a value, which `.KV.Has` reports.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KVChange" }
          application/yaml:
            schema: { $ref: "#/components/schemas/KVChange" }
      responses:
        "200":
          description: The key and its value.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KVEntry" }
            application/yaml:
              schema: { $ref: "#/components/schemas/KVEntry" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
    delete:
      tags: [KV]
      operationId: deleteKV
      summary: Remove a key
      description: A template that reads the key next fails, as for a key never set. A key that isn't set answers `204` too.
      responses:
        "204":
          description: The key isn't set.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /requests:
    get:
      tags: [Journal]
      operationId: listRequests
      summary: List requests
      description: The newest requests the server served, a page at a time, and how many it holds.
      parameters:
        - name: limit
          in: query
          description: The most requests to answer. `0` answers the count only.
          schema: { type: integer, minimum: 0, maximum: 1000, default: 100 }
        - name: before
          in: query
          description: Answer the requests before this one, the `next` of a page.
          schema: { type: integer, minimum: 0 }
        - name: after
          in: query
          description: Answer the requests after this one, such as the `mark` of a list taken before a test.
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: The requests.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RequestList" }
            application/yaml:
              schema: { $ref: "#/components/schemas/RequestList" }
        "400": { $ref: "#/components/responses/InvalidParameter" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
    query:
      tags: [Journal]
      operationId: queryRequests
      summary: Find and count requests
      description: |
        The requests the query of the body selects, newest first, and their `count`: verify a request came by its
        count, as `limit: 0` answers alone. Each key narrows the requests; an empty body selects them all. The
        indexed keys, `kind`, `outcome`, `mock_id`, `method`, `path` and `status`, select without reading the
        requests. `request` is the request part of a mock of the server's protocol, as a mock file writes it: it
        matches as a mock does, reading each request the other keys leave, so send `method` and `path` as keys of
        their own too. Only HTTP mock servers take `request`.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RequestQuery" }
          application/yaml:
            schema: { $ref: "#/components/schemas/RequestQuery" }
      responses:
        "200":
          description: The requests.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RequestList" }
            application/yaml:
              schema: { $ref: "#/components/schemas/RequestList" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
    delete:
      tags: [Journal]
      operationId: clearRequests
      summary: Clear the journal
      responses:
        "204":
          description: The journal holds no request.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /requests/verify:
    query:
      tags: [Journal]
      operationId: verifyRequests
      summary: Verify requests
      description: |
        Checks the requests of the journal as a Go test does with package `verify`, and answers its verdict: `200`
        whether it passed or not. The keys of `QUERY /requests`, but `limit` and `before`, select the requests;
        `count` matches how many, at least 1 when it's left out. The check runs now, or waits: up to `within` for the
        count to pass, or `during` for it to fail, waking as each request comes, up to 30s. `in_order`, a list of
        queries, checks instead that a request of each came after the request of the one before. `after`, the `mark`
        of a list, leaves out the requests up to it, such as those of earlier tests. A check that failed lists the
        requests that tell why, without their headers or bodies, which `GET /requests/{id}` answers.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RequestCheck" }
          application/yaml:
            schema: { $ref: "#/components/schemas/RequestCheck" }
      responses:
        "200":
          description: The verdict.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RequestVerdict" }
            application/yaml:
              schema: { $ref: "#/components/schemas/RequestVerdict" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/IncompleteJournal" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
        "503": { $ref: "#/components/responses/Closing" }
  /requests/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The id of the request, as the `VLatte-Request-Id` header of its answer names it.
        schema: { type: string }
    get:
      tags: [Journal]
      operationId: getRequest
      summary: Get a request
      responses:
        "200":
          description: The request.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JournalEntry" }
            application/yaml:
              schema: { $ref: "#/components/schemas/JournalEntry" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/RequestNotFound" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /events:
    get:
      tags: [Streams]
      operationId: streamEvents
      summary: Follow what the server serves
      description: |
        Streams what the mock server serves, as it happens, as Server-Sent Events: each event's type is the `type` of
        its data, a JSON `Event`. Nothing is kept: a client gets the events that come while it listens, and one that
        falls behind misses some. The journal, `/requests`, keeps the requests.
      responses:
        "200":
          description: The stream of events.
          content:
            text/event-stream:
              schema:
                type: string
                description: "Server-Sent Events, such as `event: matched` then `data: {\"type\": \"matched\", ...}`."
              example: |
                event: matched
                data: {"method":"GET","mock_id":"hello","request_id":"7f3a","status":200,"target":"http://127.0.0.1:8080/hello","time":"2026-10-20T12:00:00Z","type":"matched"}

        "401": { $ref: "#/components/responses/Unauthorized" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /logs:
    get:
      tags: [Streams]
      operationId: streamLogs
      summary: Follow the server's log
      description: |
        Streams the log lines of the mock server, as it writes them, as Server-Sent Events of type `log`: each one's
        data is a JSON log line. Nothing is kept. A server that logs through a logger it was given streams none.
      responses:
        "200":
          description: The stream of log lines.
          content:
            text/event-stream:
              schema:
                type: string
                description: "Server-Sent Events: `event: log` then `data: {\"level\": \"info\", ...}`."
              example: |
                event: log
                data: {"level":"info","url":"/hello","time":"2026-10-20T12:00:00Z","message":"---> REQUEST RECEIVED GET /hello"}

        "401": { $ref: "#/components/responses/Unauthorized" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /recording:
    get:
      tags: [Recording]
      operationId: getRecording
      summary: Get the state of the recording
      responses:
        "200":
          description: The state of the recording, inactive when none runs.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RecordingStatus" }
            application/yaml:
              schema: { $ref: "#/components/schemas/RecordingStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "501": { $ref: "#/components/responses/NotSupported" }
    post:
      tags: [Recording]
      operationId: startRecording
      summary: Start a recording
      description: |
        Starts a recording with the settings of the server's `record` section, which the body changes. One recording
        runs at a time.
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RecordingSettings" }
          application/yaml:
            schema: { $ref: "#/components/schemas/RecordingSettings" }
      responses:
        "201":
          description: The recording started.
          headers:
            Location:
              description: "`/recording`"
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RecordingStatus" }
            application/yaml:
              schema: { $ref: "#/components/schemas/RecordingStatus" }
        "400": { $ref: "#/components/responses/InvalidBody" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/RecordingActive" }
        "413": { $ref: "#/components/responses/BodyTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "501": { $ref: "#/components/responses/NotSupported" }
    delete:
      tags: [Recording]
      operationId: stopRecording
      summary: Stop the recording and serve it
      description: |
        Stops the recording, waits until it wrote what it took, and serves it: the mock files load again, with the
        recording's directory. Calls it couldn't record, and why it isn't served, are in `errors`: it stopped all the
        same.
      responses:
        "200":
          description: The recording stopped.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RecordingResult" }
            application/yaml:
              schema: { $ref: "#/components/schemas/RecordingResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/CrossOrigin" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
        "409": { $ref: "#/components/responses/NotRecording" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /recorder:
    get:
      tags: [Recording]
      operationId: getRecorder
      summary: Record from a browser
      description: A page that starts and stops recordings, and shows what they wrote.
      responses:
        "200":
          description: The page.
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /mcp:
    post:
      tags: [MCP]
      operationId: serveMCP
      summary: Call the admin API over MCP
      description: |
        The Model Context Protocol endpoint, over Streamable HTTP, for AI agents: a tool per operation of this API, named
        after it in snake_case, such as `add_mock`, with the operation's result in JSON. It keeps no sessions and answers
        in JSON, never a stream. A mock server that doesn't serve MCP answers `501`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: A JSON-RPC 2.0 message of the Model Context Protocol.
      responses:
        "200":
          description: The JSON-RPC 2.0 response.
          content:
            application/json:
              schema: { type: object }
        "202":
          description: The notification or response was accepted.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "501": { $ref: "#/components/responses/NotSupported" }
  /:
    get:
      tags: [Documentation]
      operationId: getRoot
      summary: Read this document at the root
      description: The page of `/docs`, at the root of the admin API, so its address alone shows the documentation.
      responses:
        "200":
          description: The page.
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /docs:
    get:
      tags: [Documentation]
      operationId: getDocs
      summary: Read this document
      description: |
        A page that renders this document. It loads its renderer, Scalar, from a CDN: the version vlatte pins, or the
        one the admin API was given.
      responses:
        "200":
          description: The page.
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /openapi:
    get:
      tags: [Documentation]
      operationId: getOpenAPI
      summary: Get this document
      description: This OpenAPI document, in JSON or YAML as `Accept` asks.
      responses:
        "200":
          description: The OpenAPI document.
          content:
            application/json:
              schema: { type: object }
            application/yaml:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "406": { $ref: "#/components/responses/NotAcceptable" }
components:
  securitySchemes:
    basic:
      type: http
      scheme: basic
      description: The username and password the admin API was given, or any username and its bearer token.
    bearer:
      type: http
      scheme: bearer
      description: The bearer token the admin API was given, in `Authorization`, never in the query.
  requestBodies:
    Mock:
      required: true
      description: A mock file, in JSON or YAML.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Mock" }
        application/yaml:
          schema: { $ref: "#/components/schemas/Mock" }
  schemas:
    Health:
      type: object
      required: [status]
      properties:
        status:
          type: string
          const: ok
        kind:
          type: string
          description: The protocol of the mock server. A probe answered without credentials leaves it out.
          examples: [http, grpc]
        url:
          type: string
          description: |
            Where the mock server serves its mocks: its URL, such as `http://127.0.0.1:8080`, or its address for gRPC,
            such as `127.0.0.1:50051`. A probe answered without credentials leaves it out.
    Mock:
      type: object
      description: |
        A mock file. For HTTP mock servers, `mockhttp/schema/http.schema.json` in the vlatte repository describes it.
      properties:
        id:
          type: string
          pattern: "^[A-Za-z0-9._~-]+$"
      additionalProperties: true
      examples:
        - id: hello
          request: { method: GET, path: /hello }
          response: { status: 200, body: hello }
    MockSummary:
      type: object
      description: The state of a mock.
      required: [id, priority, enabled, hits]
      properties:
        id:
          type: string
        name:
          type: string
        priority:
          type: integer
          description: Mocks of lower priority match first.
        enabled:
          type: boolean
          description: Whether the mock matches requests.
        hits:
          type: integer
          description: How many requests the mock answered.
        origin:
          type: string
          description: Where the mock came from, such as a mock file's path, or `admin` for this API.
      additionalProperties: false
    MockList:
      type: object
      required: [mocks]
      properties:
        mocks:
          type: array
          items: { $ref: "#/components/schemas/MockSummary" }
      additionalProperties: false
    MockChange:
      type: object
      properties:
        enabled:
          type: boolean
          description: Whether the mock matches requests.
      additionalProperties: false
    Event:
      type: object
      description: Something that happened on the mock server, the data of an event of `GET /events`.
      required: [type, time]
      properties:
        type:
          type: string
          enum: [matched, unmatched, proxied, warning, error, scenario]
          description: |
            `matched`: a mock answered a request. `unmatched`: no mock matched it. `proxied`: the server proxied it.
            `warning`: something went wrong without failing the request, such as a webhook. `error`: it failed.
            `scenario`: a scenario moved, by the mock a request picked, or by the admin API or the server's own methods;
            `target` names it, in any case, as names ignore case, or is missing when every scenario was reset, and
            `message` says where it moved.
        time:
          type: string
          format: date-time
        request_id:
          type: string
        method:
          type: string
          description: The method of the request, such as `GET`.
        target:
          type: string
          description: What the request asked for, such as the URL of an HTTP request, or the scenario that moved.
        mock_id:
          type: string
          description: The mock that matched, if one did.
        status:
          type: integer
          description: The status of the answer, when there was one.
        message:
          type: string
          description: What went wrong, or where a scenario moved.
      additionalProperties: false
    SaveSettings:
      type: object
      properties:
        dir:
          type: string
          description: |
            Directory to write the mock files into: a relative path, which starts at the server's root directory and
            stays inside it. Without one, the `_mocks` folder of the server's work directory.
        format:
          type: string
          enum: [json, yaml, yml]
          default: yaml
      additionalProperties: false
    SavedFiles:
      type: object
      required: [files]
      properties:
        files:
          type: array
          items: { type: string }
          description: The paths of the mock files written.
      additionalProperties: false
    Scenario:
      type: object
      required: [name, state, instances, states, transitions]
      properties:
        name:
          type: string
          description: The name its first mock gives it, or the name it was first set under when no mock names it.
        state:
          type: string
          description: The state of the shared instance, the one of requests without a key.
          examples: [started]
        instances:
          type: array
          description: >-
            The instances of keys that moved, in the order of their keys: mocks with a scenario key move an instance
            for each key of their requests. An instance not listed is in `started`.
          items: { $ref: "#/components/schemas/ScenarioInstance" }
        states:
          type: array
          description: >-
            The states its mocks name, `started` first. A scenario no mock names, but that was set, has none.
          items: { $ref: "#/components/schemas/ScenarioState" }
        transitions:
          type: array
          description: The moves its mocks make when picked to answer, in the order of the mocks.
          items: { $ref: "#/components/schemas/ScenarioTransition" }
      additionalProperties: false
    ScenarioInstance:
      type: object
      required: [key, state]
      properties:
        key:
          type: string
        state:
          type: string
      additionalProperties: false
    ScenarioState:
      type: object
      required: [name, reachable]
      properties:
        name:
          type: string
        reachable:
          type: boolean
          description: False when no path of mock moves leads to the state from `started`, so requests get there only after the scenario is set to it or to another such state.
      additionalProperties: false
    ScenarioTransition:
      type: object
      required: [to, mock_id]
      properties:
        from:
          type: string
          description: The state the mock requires. Absent when it matches in any.
        to:
          type: string
        mock_id:
          type: string
      additionalProperties: false
    ScenarioList:
      type: object
      required: [scenarios]
      properties:
        scenarios:
          type: array
          items: { $ref: "#/components/schemas/Scenario" }
      additionalProperties: false
    ScenarioChange:
      type: object
      required: [state]
      properties:
        state:
          type: string
          minLength: 1
          description: The state the scenario moves to.
      additionalProperties: false
    KVKeys:
      type: object
      required: [keys]
      properties:
        keys:
          type: array
          items: { type: string }
          description: The keys, in order.
      additionalProperties: false
    KVEntry:
      type: object
      required: [key, value]
      properties:
        key:
          type: string
        value:
          type: string
      additionalProperties: false
    KVChange:
      type: object
      required: [value]
      properties:
        value:
          type: string
          description: The value of the key, as text. An empty one is a value.
      additionalProperties: false
    RecordingSettings:
      type: object
      description: The settings of a recording, as the `record` section of the server's settings has them.
      properties:
        dir:
          type: string
          description: |
            Directory of the recordings. Relative paths start at the server's root directory, and `{workdir}` at its
            work directory.
          default: "{workdir}/_mocks_recorded"
        format:
          type: string
          enum: [json, yaml, yml]
          default: json
        target:
          type: string
          description: The service to record, such as `https://api.example.com`. Requests the recording takes go to it.
        skip_ssl_verify:
          type: boolean
          description: Skip the verification of the target's TLS certificate.
        methods:
          type: array
          items: { type: string }
          description: Methods of the requests the recording takes. None takes every method.
        paths:
          type: array
          items: { type: string }
          description: Glob patterns of the paths of the requests the recording takes, such as `/v1/*`.
        request_headers:
          type: array
          items: { type: string }
          description: Request headers recordings match.
        exclude_response_headers:
          type: array
          items: { type: string }
          description: Response headers recordings leave out.
          default: [Set-Cookie]
        body_files:
          type: boolean
          description: Write each response body to a file of its own.
        max_inline_body:
          type: integer
          minimum: 0
          description: Bytes of a body kept in memory while it is recorded. 0 uses the default.
        max_body_size:
          type: integer
          description: Calls with a larger response body aren't recorded. 0 uses the default; negative, no limit.
        repeats:
          type: string
          enum: [sequence, first]
          description: What a recording keeps of a request that comes again.
        include_mocked:
          type: boolean
          description: Record the answers of mocks too, not only proxied ones.
      additionalProperties: false
    RecordingStatus:
      type: object
      required: [active, calls, mocks, dropped, failed]
      properties:
        active:
          type: boolean
          description: Whether a recording takes calls.
        calls:
          type: integer
          description: Calls the recording took.
        mocks:
          type: integer
          description: Mocks it wrote.
        dropped:
          type: integer
          description: Calls it refused, because too many waited to be written.
        failed:
          type: integer
          description: Calls and mocks it couldn't record.
      additionalProperties: false
    RecordingResult:
      type: object
      required: [status, recordings]
      properties:
        status: { $ref: "#/components/schemas/RecordingStatus" }
        recordings:
          type: array
          description: The mock files the recording wrote, in the order their requests first came.
          items:
            type: object
            required: [id, file, responses]
            properties:
              id:
                type: string
              name:
                type: string
              file:
                type: string
                description: The path of the mock file.
              body_files:
                type: array
                items: { type: string }
                description: The names of its body files, next to it.
              responses:
                type: integer
                description: How many responses it replies with.
            additionalProperties: false
        errors:
          type: array
          items: { type: string }
          description: What the recording couldn't record, and why it isn't served.
      additionalProperties: false
    JournalEntry:
      type: object
      description: A request the server served, as its journal keeps it.
      required: [seq, id, time, kind, outcome]
      properties:
        seq:
          type: integer
          minimum: 1
          description: Orders the requests of the journal, a later one has a greater one.
        id:
          type: string
          description: The id of the request.
        time:
          type: string
          format: date-time
          description: When the request came.
        kind:
          type: string
          description: The protocol of the request, such as `http` or `grpc`.
        outcome:
          type: string
          enum: [matched, unmatched, proxied, failed, abandoned]
          description: |
            `matched`: a mock answered. `unmatched`: no mock matched. `proxied`: another server answered. `failed`:
            serving it failed. `abandoned`: its client left before its answer.
        mock_id:
          type: string
          description: The mock that matched, if one did.
        method:
          type: string
          description: The method, such as `GET`. gRPC calls have none.
        path:
          type: string
          description: The path, or the full method of a gRPC call.
        target:
          type: string
          description: What the request asked for in full, such as its URL.
        status:
          type: integer
          description: The status of the answer, such as an HTTP status or a gRPC code.
        elapsed_ns:
          type: integer
          description: How long serving it took, in nanoseconds.
        header:
          type: object
          description: The headers, or the metadata, by name. Those the server redacts are redacted.
          additionalProperties:
            type: array
            items: { type: string }
        body:
          type: string
          description: The body, when it is text.
        body_base64:
          type: string
          contentEncoding: base64
          description: The body, when it isn't text.
        body_truncated:
          type: boolean
          description: The body kept is less than the request's.
        message:
          type: string
          description: What went wrong, for a request that failed or was abandoned.
        near_misses:
          type: array
          description: The mocks that came closest to matching a request no mock matched, the closest first.
          items: { $ref: "#/components/schemas/NearMiss" }
        scenarios:
          type: array
          description: The moves of the scenarios of the mock that matched the request, made as the server picked it.
          items: { $ref: "#/components/schemas/ScenarioMove" }
      additionalProperties: false
    ScenarioMove:
      type: object
      required: [scenario, to]
      properties:
        scenario:
          type: string
          description: The name of the scenario, as the mock gives it.
        key:
          type: string
          description: The key of the instance the request moved; missing for the shared instance.
        from:
          type: string
          description: The state the mock requires; missing when it requires none.
        to:
          type: string
          description: The state the mock moved the scenario to.
      additionalProperties: false
    NearMiss:
      type: object
      required: [mock_id, passed, total]
      properties:
        mock_id:
          type: string
        name:
          type: string
        passed:
          type: integer
          description: How many parts of the request the mock checked matched.
        total:
          type: integer
          description: How many parts it checked.
        failed:
          type: array
          items: { type: string }
          description: The parts that differ, such as `header Accept`.
        skipped:
          type: array
          items: { type: string }
          description: The parts it didn't check, which keep state, such as `times`.
      additionalProperties: false
    RequestList:
      type: object
      required: [count, requests, mark]
      properties:
        count:
          type: integer
          description: How many requests the query selects, whatever the page.
        requests:
          type: array
          description: A page of them, the newest first.
          items: { $ref: "#/components/schemas/JournalEntry" }
        next:
          type: integer
          description: The `before` of the next page, when there is one.
        mark:
          type: integer
          description: |
            The number of the newest request of the journal. A query or a check that takes it as `after` leaves out
            the requests up to it.
      additionalProperties: false
    Matcher:
      description: |
        Text, which matches equal text, a number or a bool, which matches an equal value, or a matcher list, as mock
        files write them, such as `["glob", "/users/*"]`.
      anyOf:
        - type: string
        - type: number
        - type: boolean
        - type: array
    RequestQuery:
      type: object
      properties:
        kind:
          type: string
          description: The protocol, such as `http`.
        outcome:
          type: string
          enum: [matched, unmatched, proxied, failed, abandoned]
        mock_id: { $ref: "#/components/schemas/Matcher" }
        method: { $ref: "#/components/schemas/Matcher" }
        path: { $ref: "#/components/schemas/Matcher" }
        target: { $ref: "#/components/schemas/Matcher" }
        status: { $ref: "#/components/schemas/Matcher" }
        body:
          $ref: "#/components/schemas/Matcher"
          description: Matches the body as text. A JSON path matcher reads it as JSON.
        header:
          type: object
          description: Headers, or metadata, by name, in any case. A request without the header has it empty.
          additionalProperties: { $ref: "#/components/schemas/Matcher" }
        since:
          type: string
          format: date-time
          description: The earliest time of the requests.
        until:
          type: string
          format: date-time
          description: The time the requests came before.
        request:
          type: object
          description: |
            The request part of a mock of the server's protocol, as a mock file writes it, such as
            `{"query": {"page": "2"}}`.
        limit:
          type: integer
          minimum: 0
          maximum: 1000
          default: 100
          description: The most requests to answer. `0` answers the count only.
        before:
          type: integer
          minimum: 0
          description: Answer the requests before this one, the `next` of a page.
        after:
          type: integer
          minimum: 0
          description: Answer the requests after this one, such as the `mark` of a list taken before a test.
      additionalProperties: false
    RequestCheck:
      type: object
      description: |
        The keys of a `RequestQuery`, but `limit` and `before`, select the requests the check counts. `in_order` takes
        their place, and `count`'s, for a check of order.
      properties:
        kind: { $ref: "#/components/schemas/RequestQuery/properties/kind" }
        outcome: { $ref: "#/components/schemas/RequestQuery/properties/outcome" }
        mock_id: { $ref: "#/components/schemas/Matcher" }
        method: { $ref: "#/components/schemas/Matcher" }
        path: { $ref: "#/components/schemas/Matcher" }
        target: { $ref: "#/components/schemas/Matcher" }
        status: { $ref: "#/components/schemas/Matcher" }
        body: { $ref: "#/components/schemas/RequestQuery/properties/body" }
        header: { $ref: "#/components/schemas/RequestQuery/properties/header" }
        since: { $ref: "#/components/schemas/RequestQuery/properties/since" }
        until: { $ref: "#/components/schemas/RequestQuery/properties/until" }
        request: { $ref: "#/components/schemas/RequestQuery/properties/request" }
        after:
          type: integer
          minimum: 0
          description: |
            Check the requests after this one, the `mark` of a list. A journal cleared since answers
            `incomplete_journal`.
        count:
          $ref: "#/components/schemas/Matcher"
          description: How many requests, such as `2` or `["gte", 2]`. None is at least 1.
        within:
          $ref: "#/components/schemas/Wait"
          description: Wait up to this long for the count to pass, such as for a request sent in the background.
        during:
          $ref: "#/components/schemas/Wait"
          description: Wait this long for the count to fail, such as for `count` 0. It passes when it ends.
        in_order:
          type: array
          minItems: 1
          description: |
            The steps of an order, each the keys of a query that select its requests, but `after`, which is the
            check's. A step's request comes after the request of the step before; other requests may come between.
            It checks now.
          items:
            type: object
            properties:
              kind: { $ref: "#/components/schemas/RequestQuery/properties/kind" }
              outcome: { $ref: "#/components/schemas/RequestQuery/properties/outcome" }
              mock_id: { $ref: "#/components/schemas/Matcher" }
              method: { $ref: "#/components/schemas/Matcher" }
              path: { $ref: "#/components/schemas/Matcher" }
              target: { $ref: "#/components/schemas/Matcher" }
              status: { $ref: "#/components/schemas/Matcher" }
              body: { $ref: "#/components/schemas/RequestQuery/properties/body" }
              header: { $ref: "#/components/schemas/RequestQuery/properties/header" }
              since: { $ref: "#/components/schemas/RequestQuery/properties/since" }
              until: { $ref: "#/components/schemas/RequestQuery/properties/until" }
              request: { $ref: "#/components/schemas/RequestQuery/properties/request" }
            additionalProperties: false
      additionalProperties: false
    Wait:
      description: |
        How long to wait, up to 30s: whole milliseconds, or a duration, such as `1.5s`. The wait ends with the server,
        and with a reset of its journal.
      anyOf:
        - type: integer
          minimum: 1
          maximum: 30000
        - type: string
    RequestVerdict:
      type: object
      required: [passed, waited_ms]
      properties:
        passed:
          type: boolean
        count:
          type: integer
          description: How many requests the check selected. A check of order has none.
        waited_ms:
          type: integer
          description: How long the check waited, in milliseconds.
        selected:
          type: array
          description: Of a check that failed, the requests it selected, the newest first, up to 10.
          items: { $ref: "#/components/schemas/ListedRequest" }
        closest:
          type: array
          description: |
            Of a check that failed for want of requests, those that came closest to being selected, up to 10: the
            fewest parts missed first, then the newest.
          items: { $ref: "#/components/schemas/ListedRequest" }
        report:
          type: string
          description: Of a check that failed, why, in lines, as a Go test reports it.
      additionalProperties: false
    ListedRequest:
      type: object
      description: A request a verdict lists, without its headers or body, which may hold secrets.
      required: [seq, id, line]
      properties:
        seq:
          type: integer
          description: The number of the request in the journal.
        id:
          type: string
          description: The id of the request, for `GET /requests/{id}`.
        line:
          type: string
          description: The request in a line, such as `#3 2026-10-21T12:00:00.000Z matched GET http://127.0.0.1/a 200`.
        passed:
          type: integer
          description: Of a request that came close, how many of the parts of the check it passed.
        total:
          type: integer
          description: Of a request that came close, how many parts of the check it was checked by.
        missed:
          type: array
          items: { type: string }
          description: Of a request that came close, the parts of the check it missed, such as `header Accept`.
      additionalProperties: false
    Problem:
      type: object
      description: An RFC 9457 problem document. Its type is `about:blank`, so `title` is the status's phrase.
      required: [status, title, code, detail, instance]
      properties:
        status:
          type: integer
        title:
          type: string
        code:
          type: string
          description: What went wrong, as a stable code clients can act on.
          enum:
            - invalid_body
            - id_mismatch
            - cross_origin
            - mock_not_found
            - route_not_found
            - method_not_allowed
            - not_acceptable
            - duplicate_id
            - body_too_large
            - unsupported_media_type
            - describe_failed
            - load_failed
            - internal
            - not_supported
            - recording_active
            - not_recording
            - closing
            - request_not_found
            - invalid_parameter
            - not_ready
            - unknown_state
            - unauthorized
            - incomplete_journal
            - key_not_found
        detail:
          type: string
          description: What went wrong in this request.
        instance:
          type: string
          description: The path of the request.
        problems:
          type: array
          description: The problems of a mock, a body or a mock file, one by one.
          items: { $ref: "#/components/schemas/ProblemItem" }
      additionalProperties: false
    ProblemItem:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: A stable code, such as `unknown_key`, `invalid_value` or `parse_error`.
        origin:
          type: string
          description: Where the problem is, such as `admin request` or a mock file.
        key:
          type: string
          description: The key with the problem, such as `response.status`.
        value:
          type: string
          description: The value with the problem.
        message:
          type: string
        hint:
          type: string
          description: How to fix it.
      additionalProperties: false
  responses:
    InvalidBody:
      description: "`invalid_body`: the mock or the body has problems, or `id_mismatch`: the body's id isn't the path's."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    CrossOrigin:
      description: "`cross_origin`: a browser's cross-site request can't change the server."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    MockNotFound:
      description: "`mock_not_found`: no mock has the id."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    KeyNotFound:
      description: "`key_not_found`: the key-value store holds no value for the key."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    NotAcceptable:
      description: "`not_acceptable`: `Accept` takes neither JSON nor YAML. Always JSON."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    DuplicateID:
      description: "`duplicate_id`: a mock has the id already."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    BodyTooLarge:
      description: "`body_too_large`: the body is over 10 MiB."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    UnsupportedMediaType:
      description: "`unsupported_media_type`: the body is neither JSON nor YAML. `Accept` lists what is."
      headers:
        Accept:
          schema: { type: string }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    DescribeFailed:
      description: "`describe_failed`: the mock can't be described as a mock file, such as one with a binary body."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    SaveFailed:
      description: |
        `describe_failed`: a mock can't be described as a mock file, and nothing was written, or `load_failed`: the
        mock files, written, failed to load, or couldn't be written.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    LoadFailed:
      description: "`load_failed`: the mock files have problems, listed in `problems`. Nothing changed."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    RecordingActive:
      description: "`recording_active`: a recording runs already."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    NotRecording:
      description: "`not_recording`: no recording runs."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    RequestNotFound:
      description: "`request_not_found`: the journal holds no request of the id."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    InvalidParameter:
      description: "`invalid_parameter`: a query parameter has a value the route doesn't take."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    NotReady:
      description: "`not_ready`: the mock store can't serve its mocks, such as when the database behind it is down."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    NotSupported:
      description: "`not_supported`: the mock server's protocol can't do this."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    IncompleteJournal:
      description: |
        `incomplete_journal`: the journal no longer holds every request the check counts: it dropped some to keep
        its most entries, it was cleared after `after` or during the wait, or `after` is past its newest request; or
        it kept part of a body the check reads.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    Closing:
      description: "`closing`: the mock server is closing, and ended the wait."
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
    Unauthorized:
      description: |
        `unauthorized`: the admin API asks for credentials, and the request has none, or not those. `WWW-Authenticate`
        names the schemes it takes.
      headers:
        WWW-Authenticate:
          schema: { type: string }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
        application/problem+yaml:
          schema: { $ref: "#/components/schemas/Problem" }
