{
  "openapi": "3.1.0",
  "info": {
    "title": "Dokkit API",
    "version": "0.1.0",
    "description": "Dokkit watches machines, services and jobs. This is the HTTP API that the `dokkit` program, the command line, the client libraries, the MCP server and any script use."
  },
  "servers": [
    {
      "url": "https://your.domain.com"
    }
  ],
  "tags": [
    {
      "name": "Intake",
      "description": "Machines and apps send reports, deploys and declarations. Machine key or app key."
    },
    {
      "name": "Errors",
      "description": "Error reports from an app, in Dokkit's own format or as Sentry envelopes. App key."
    },
    {
      "name": "Work",
      "description": "The `dokkit` program asks for work, sends its output and result, and fetches updates. Machine key."
    },
    {
      "name": "Read",
      "description": "Read issues, machines, services, jobs, logs, errors, deploys, rules and proposals. User token."
    },
    {
      "name": "Act",
      "description": "Snooze and acknowledge issues, route and hold services, accept found things, keep error text and set when a job is late. User token with `act`."
    },
    {
      "name": "Admin",
      "description": "Keys, people, alert rules and settings, interface-added checks, restart, making jobs, updating the program and loading logs back. User token with `admin`."
    },
    {
      "name": "MCP",
      "description": "The MCP server for AI tools. User token."
    }
  ],
  "paths": {
    "/api/v1/enrol": {
      "post": {
        "operationId": "enrol",
        "summary": "Trade an enrolment token for a machine key",
        "tags": [
          "Intake"
        ],
        "description": "Spends a one-use enrolment token and makes the machine. The answer holds the machine key, shown once. A name that is already enrolled answers 409 and the token is kept, unless the token was made for that machine; then it enrols again and revokes that machine's earlier keys. Takes no key in the header. The enrolment token (`dke_`) goes in the body.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "hostname"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The `dke_` token."
                  },
                  "hostname": {
                    "type": "string"
                  },
                  "os": {
                    "type": "string"
                  },
                  "arch": {
                    "type": "string"
                  },
                  "dokkit": {
                    "type": "string",
                    "description": "The version of the `dokkit` program."
                  }
                }
              },
              "example": {
                "token": "dke_3kQ9xV2mTn7LwRb8",
                "hostname": "db-01",
                "os": "windows",
                "arch": "amd64",
                "dokkit": "0.1.0"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The machine and its key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "machine": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "key": {
                      "type": "string",
                      "description": "The `dkm_` machine key. Shown once."
                    }
                  }
                },
                "example": {
                  "machine": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "key": "dkm_7Hq2pXz4Yc9VnB1a"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_token",
                    "message": "That token is not known, was already used, or has run out."
                  }
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "already_enrolled",
                    "message": "That machine name is already enrolled. Ask for an enrolment token made for that machine."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_body",
                    "message": "Needs a token and a hostname."
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/report": {
      "post": {
        "operationId": "report",
        "summary": "Send a batch of lines",
        "tags": [
          "Intake"
        ],
        "description": "The main door. The body is newline-delimited JSON, one item per line, with a `type` and an `at` or `t`. The types are `state`, `inventory`, `metric`, `beat`, `job`, `stats`, `log`, `deploy` and `custom`. A batch is at most 5 MB compressed and 10,000 lines. A refused line does not fail the batch. Send the same `Dokkit-Batch` id again and nothing is stored twice. An app key may not send `state` or `inventory`, and may make 100 checks per service. Any line may carry `environment`: a value other than `production` is stored on a sibling service of that environment that never alerts, unless the service is declared by a `dokkit.toml`, which keeps its own environment. Takes a machine key (`dkm_`) or an app key (`dka_`).",
        "parameters": [
          {
            "name": "Dokkit-Batch",
            "in": "header",
            "description": "Names the batch. Sending it again stores nothing twice.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false
          },
          {
            "name": "Content-Encoding",
            "in": "header",
            "description": "Set to gzip when the body is compressed.",
            "schema": {
              "type": "string",
              "enum": [
                "gzip"
              ]
            },
            "required": false
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "One JSON object per line."
              },
              "example": "{\"type\":\"job\",\"service\":\"nightly-purge\",\"name\":\"nightly-purge\",\"run\":\"r-2041\",\"event\":\"ok\",\"exit_code\":0,\"duration_ms\":81234}\n{\"type\":\"log\",\"t\":\"2026-10-05T21:14:03.120Z\",\"level\":\"error\",\"msg\":\"Upload failed\",\"service\":\"upload-portal\",\"fields\":{\"route\":\"/upload\",\"status\":500}}\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many lines were accepted, and the ones refused.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accepted": {
                      "type": "integer"
                    },
                    "refused": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Refusal"
                      }
                    }
                  }
                },
                "example": {
                  "accepted": 1,
                  "refused": [
                    {
                      "line": 2,
                      "code": "unknown_service",
                      "message": "No service \"upload-portal\" on this machine."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "413": {
            "description": "Too large.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_large",
                    "message": "A batch is at most 5 MB compressed and 10,000 lines."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The batch id is not a UUID (`bad_batch`), or the body is not valid gzip (`bad_gzip`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_batch",
                    "message": "Dokkit-Batch must be a UUID."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          },
          {
            "appKey": []
          }
        ]
      },
      "options": {
        "operationId": "reportPreflight",
        "summary": "Browser preflight for report",
        "tags": [
          "Intake"
        ],
        "description": "Answers a browser's preflight check with the CORS headers and no body. App keys are write-only and ship inside browser pages, so every origin is allowed. Takes no key.",
        "responses": {
          "204": {
            "description": "No content. The CORS headers are set."
          }
        },
        "security": []
      }
    },
    "/api/v1/in/{key}/job/{name}/{event}": {
      "get": {
        "operationId": "pingGet",
        "summary": "Ping a job with a web request",
        "tags": [
          "Intake"
        ],
        "description": "For anything that can only make a web request, such as a cron job on a storage box. `start`, `ok` and `fail` record the run. Any other event answers 404. Takes an app key (`dka_`).",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "description": "An app key (`dka_`). It travels in the address because the callers can only make a web request.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "name",
            "in": "path",
            "description": "The job's name.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "event",
            "in": "path",
            "description": "What happened.",
            "schema": {
              "type": "string",
              "enum": [
                "start",
                "ok",
                "fail"
              ]
            },
            "required": true
          },
          {
            "name": "run",
            "in": "query",
            "description": "Ties a start to its finish.",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such address."
                  }
                }
              }
            }
          },
          "413": {
            "description": "Too large.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_large",
                    "message": "Too large."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_many_checks",
                    "message": "A service takes at most 100 checks from an app key."
                  }
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "operationId": "pingPost",
        "summary": "Ping a job and keep its output",
        "tags": [
          "Intake"
        ],
        "description": "Same as the GET. The body, if there is one, is kept as the last lines of the job's output, at most 16 KB. Any content type is accepted. Takes an app key (`dka_`).",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "description": "An app key (`dka_`). It travels in the address because the callers can only make a web request.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "name",
            "in": "path",
            "description": "The job's name.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "event",
            "in": "path",
            "description": "What happened.",
            "schema": {
              "type": "string",
              "enum": [
                "start",
                "ok",
                "fail"
              ]
            },
            "required": true
          },
          {
            "name": "run",
            "in": "query",
            "description": "Ties a start to its finish.",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "The last lines of output."
              },
              "example": "Purged 412 files.\nDone.\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such address."
                  }
                }
              }
            }
          },
          "413": {
            "description": "Too large.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_large",
                    "message": "Too large."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_many_checks",
                    "message": "A service takes at most 100 checks from an app key."
                  }
                }
              }
            }
          }
        },
        "security": []
      },
      "options": {
        "operationId": "pingPreflight",
        "summary": "Browser preflight for the ping URLs",
        "tags": [
          "Intake"
        ],
        "description": "Answers a browser's preflight check with the CORS headers and no body. App keys are write-only and ship inside browser pages, so every origin is allowed. Takes no key.",
        "responses": {
          "204": {
            "description": "No content. The CORS headers are set."
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "description": "An app key (`dka_`). It travels in the address because the callers can only make a web request.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "name",
            "in": "path",
            "description": "The job's name.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "event",
            "in": "path",
            "description": "What happened.",
            "schema": {
              "type": "string",
              "enum": [
                "start",
                "ok",
                "fail"
              ]
            },
            "required": true
          }
        ]
      }
    },
    "/api/v1/declare": {
      "put": {
        "operationId": "declare",
        "summary": "Declare a service from a dokkit.toml",
        "tags": [
          "Intake"
        ],
        "description": "The contents of a `dokkit.toml`, sent as JSON with the same structure. Sending the same file twice changes nothing. Checks, jobs, logs and stats become active. The file owns the service and its checks, so checks it no longer lists are removed. A machine key may declare any service on its machine. An app key may declare only the service it belongs to: `service.id` must be its slug (else 422), the machine and kind stay, checks may be of type `beat` or `custom` only, jobs keep `id`, `schedule`, `grace` and `max_runtime`, and the other sections are dropped. Takes a machine key (`dkm_`) or an app key (`dka_`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "schema",
                  "service"
                ],
                "properties": {
                  "schema": {
                    "type": "integer",
                    "enum": [
                      1
                    ]
                  },
                  "service": {
                    "type": "object",
                    "required": [
                      "id"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "pattern": "^[a-z0-9][a-z0-9._-]*$"
                      },
                      "name": {
                        "type": "string"
                      },
                      "kind": {
                        "type": "string",
                        "enum": [
                          "service",
                          "job",
                          "dependency",
                          "app"
                        ]
                      },
                      "version": {
                        "type": "string"
                      }
                    }
                  },
                  "check": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "One check: type, name and its settings."
                    }
                  },
                  "job": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "One job: name and schedule."
                    }
                  }
                }
              },
              "example": {
                "schema": 1,
                "service": {
                  "id": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service"
                },
                "check": [
                  {
                    "type": "http",
                    "name": "front-page",
                    "url": "http://localhost:8080/health",
                    "expect": 200
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "A declaration needs a machine key."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_declaration",
                    "message": "This server reads schema 1."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/ping": {
      "get": {
        "operationId": "hello",
        "summary": "Check the address, the key and the clock",
        "tags": [
          "Intake"
        ],
        "description": "Lets a machine check its address, its key and its clock. The answer holds the server's time. Takes a machine key (`dkm_`) or an app key (`dka_`).",
        "responses": {
          "200": {
            "description": "The key works.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "time": "2026-10-05T21:14:03.120000Z"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          },
          {
            "appKey": []
          }
        ]
      }
    },
    "/api/v1/proposals": {
      "post": {
        "operationId": "propose",
        "summary": "Send what a scan found",
        "tags": [
          "Intake"
        ],
        "description": "What `dokkit scan` found, as the file was written. Nothing in it is active until a person accepts it in the interface. The server keeps running Windows services and systemd services, listening ports, scheduled jobs (`found.jobs`), containers that publish a TCP port and sites with an address, and leaves out what is already watched or was dismissed. A job is accepted as a job that waits for its first report. `notes` (a list of text) says what the scan could not see. Takes a machine key (`dkm_`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "found"
                ],
                "properties": {
                  "os": {
                    "type": "string"
                  },
                  "found": {
                    "type": "object",
                    "properties": {
                      "services": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "display": {
                              "type": "string"
                            },
                            "state": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "ports": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "port": {
                              "type": "integer"
                            },
                            "program": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "os": "windows",
                "found": {
                  "services": [
                    {
                      "name": "Spooler",
                      "display": "Print Spooler",
                      "state": "running"
                    }
                  ],
                  "ports": [
                    {
                      "port": 5432,
                      "program": "postgres.exe"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many things from this machine now wait.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "waiting": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "waiting": 2
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_proposal",
                    "message": "Needs what dokkit scan wrote."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      },
      "get": {
        "operationId": "listProposals",
        "summary": "List things waiting to be accepted",
        "tags": [
          "Read"
        ],
        "description": "Found things that wait for a person to accept them, each with `origin` (`system`, `software` or `yours`; null when the machine's program did not say) and `why`. The operating system's own things (`origin` `system`) are left out unless `system` is true; the answer says how many in `hidden`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Proposal"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    },
                    "hidden": {
                      "type": "object",
                      "description": "What was left out because it is the operating system's own, under the same machine, kind and q filters. Zero with system true.",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "machines": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "The count for each machine name."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "kind": "service",
                      "name": "Spooler",
                      "detail": {
                        "label": "Print Spooler",
                        "saw": "Running"
                      },
                      "machine": "db-01",
                      "origin": "software",
                      "why": "Installed under C:\\Program Files\\Contoso"
                    }
                  ],
                  "next": null,
                  "hidden": {
                    "total": 12,
                    "machines": {
                      "db-01": 12
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "service, port, job, container or site.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "system",
            "in": "query",
            "description": "true includes the operating system's own findings (origin system), which are left out otherwise.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      }
    },
    "/api/v1/deploys": {
      "post": {
        "operationId": "deploy",
        "summary": "Record a deploy from CI",
        "tags": [
          "Intake"
        ],
        "description": "For CI. `host` is the machine the version went onto, when that is not the machine the service lives on. A deploy is also recorded when a service's reported version changes, so most apps never call this. Takes a machine key (`dkm_`) or an app key (`dka_`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version"
                ],
                "properties": {
                  "service": {
                    "type": "string",
                    "description": "The service slug. Machine keys only; an app key is tied to one service."
                  },
                  "version": {
                    "type": "string"
                  },
                  "host": {
                    "type": "string"
                  },
                  "commit": {
                    "type": "string"
                  },
                  "by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "service": "upload-portal",
                "version": "2.3.0",
                "host": "web-02",
                "commit": "abc123",
                "by": "ci"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_line",
                    "message": "A deploy needs a version."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          },
          {
            "appKey": []
          }
        ]
      },
      "get": {
        "operationId": "listDeploys",
        "summary": "List deploys",
        "tags": [
          "Read"
        ],
        "description": "Deploy history, newest first. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Deploy"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "Upload portal",
                      "machine": "web-02",
                      "version": "2.3.0",
                      "from": "2.2.9",
                      "commit": "abc123",
                      "by": "ci",
                      "how": "reported",
                      "at": "2026-10-05T19:30:00Z"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "Only this service (id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "environment",
            "in": "query",
            "description": "Only in this environment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only from this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only up to this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      }
    },
    "/api/v1/checks": {
      "get": {
        "operationId": "checks",
        "summary": "Get the checks added in the interface",
        "tags": [
          "Intake"
        ],
        "description": "The checks added in the interface for this machine, for the `dokkit` program to run. Takes a machine key (`dkm_`).",
        "responses": {
          "200": {
            "description": "The checks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "service": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "config": {
                            "type": "object",
                            "description": "The check's settings."
                          },
                          "restart": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "service": "spooler",
                      "name": "windows-service",
                      "type": "windows-service",
                      "config": {
                        "name": "Spooler"
                      },
                      "restart": false
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/errors": {
      "post": {
        "operationId": "reportError",
        "summary": "Report an error in Dokkit's format",
        "tags": [
          "Errors"
        ],
        "description": "One error report. Reports are grouped by `type` and the app's own frames. Message text is dropped unless the service's page is set to keep it. An error does not open an issue or alert anyone. Takes a machine key (`dkm_`) or an app key (`dka_`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type"
                ],
                "properties": {
                  "at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "service": {
                    "type": "string"
                  },
                  "release": {
                    "type": "string"
                  },
                  "environment": {
                    "type": "string"
                  },
                  "host": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "description": "The error's class or type name."
                  },
                  "handled": {
                    "type": "boolean"
                  },
                  "message": {
                    "type": "string",
                    "description": "Optional, and kept only if the service is set to keep messages."
                  },
                  "frames": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "file": {
                          "type": "string"
                        },
                        "function": {
                          "type": "string"
                        },
                        "line": {
                          "type": "integer"
                        },
                        "own": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "tags": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "at": "2026-10-05T21:14:03.120Z",
                "service": "upload-portal",
                "release": "2.3.0",
                "environment": "production",
                "host": "web-02",
                "type": "TimeoutError",
                "handled": true,
                "frames": [
                  {
                    "file": "app/worker.py",
                    "function": "run_job",
                    "line": 212,
                    "own": true
                  }
                ],
                "tags": {
                  "route": "/upload",
                  "status": "500"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_report",
                    "message": "An error needs a type."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          },
          {
            "appKey": []
          }
        ]
      },
      "options": {
        "operationId": "reportErrorPreflight",
        "summary": "Browser preflight for errors",
        "tags": [
          "Errors"
        ],
        "description": "Answers a browser's preflight check with the CORS headers and no body. App keys are write-only and ship inside browser pages, so every origin is allowed. Takes no key.",
        "responses": {
          "204": {
            "description": "No content. The CORS headers are set."
          }
        },
        "security": []
      },
      "get": {
        "operationId": "listErrors",
        "summary": "List error groups",
        "tags": [
          "Read"
        ],
        "description": "Error groups by type and place, newest first, from the newest 1,000. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ErrorGroup"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "type": "TimeoutError",
                      "top": {
                        "file": "app/worker.py",
                        "function": "run_job",
                        "line": 212,
                        "own": true
                      },
                      "count": 14,
                      "first_seen": "2026-10-05T19:40:00Z",
                      "last_seen": "2026-10-05T21:14:03Z",
                      "release": "2.3.0",
                      "host": "web-02",
                      "handled": true,
                      "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "Upload portal",
                      "machine": "web-02"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "Only this service (id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "environment",
            "in": "query",
            "description": "Only in this environment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only from this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only up to this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      }
    },
    "/api/events/raw": {
      "post": {
        "operationId": "seqEvents",
        "summary": "Report log lines from an app that logs to Seq",
        "tags": [
          "Intake"
        ],
        "description": "The door for apps and libraries written for Seq: Serilog.Sinks.Seq, seqlog, and the pino, winston and bunyan Seq transports. An app moves by changing its server address and API key. The body is CLEF (one JSON event a line, content type `application/vnd.serilog.clef`, or `?clef`), or Seq's older JSON form `{\"Events\": [...]}` with `Timestamp`, `Level`, `MessageTemplate`, `Properties`, `Exception` and `RenderedMessage`. Each event is stored as a log line of the app key's service, by the same path as `POST /api/v1/report`: the same limits (10,000 events and 5 MB a request) and no rate limit. Levels are mapped (`Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal` and the short forms), templates are rendered with the properties, properties become `fields`, an exception is kept as `fields.exception`. A property named `service`, `at` or `type` is kept as `service_name`, `seq_at` or `seq_type`. The same body is accepted at `POST /ingest/clef`, where newer Seq clients post CLEF. Takes an app key (`dka_`), in the `X-Seq-ApiKey` header or the `apiKey` query value. Answers as Seq does: `201` with `{\"MinimumLevelAccepted\": null}`, and errors as `{\"Error\": \"...\"}`.",
        "parameters": [
          {
            "name": "clef",
            "in": "query",
            "description": "Present when the body is CLEF and the content type does not say so.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "apiKey",
            "in": "query",
            "description": "The app key, when the client sends it in the query.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "X-Seq-ApiKey",
            "in": "header",
            "description": "The app key.",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/vnd.serilog.clef": {
              "schema": {
                "type": "string",
                "description": "Newline-delimited CLEF events."
              },
              "example": "{\"@t\":\"2026-10-05T21:14:03.1200000Z\",\"@mt\":\"Saved {File} in {Elapsed} ms\",\"File\":\"a.pdf\",\"Elapsed\":41}\n{\"@t\":\"2026-10-05T21:14:04.0000000Z\",\"@mt\":\"Failed {Id}\",\"@l\":\"Error\",\"@x\":\"System.IO.IOException: disk\",\"Id\":7}\n"
            },
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Seq's JSON form: an `Events` array."
              },
              "example": {
                "Events": [
                  {
                    "Timestamp": "2026-10-05T21:14:03.12+00:00",
                    "Level": "Error",
                    "MessageTemplate": "Failed {Id}",
                    "Properties": {
                      "Id": 7
                    },
                    "Exception": "System.IO.IOException: disk"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "MinimumLevelAccepted": null
                }
              }
            }
          },
          "400": {
            "description": "The body is not CLEF or Seq's JSON, or no event could be stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "Error": "The body is not CLEF or Seq's JSON events."
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not an app key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "Error": "The API key is missing or not valid."
                }
              }
            }
          },
          "413": {
            "description": "More than 10,000 events or 5 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "Error": "A request is at most 5 MB and 10,000 events."
                }
              }
            }
          }
        },
        "security": [
          {
            "appKey": []
          }
        ]
      }
    },
    "/api/{project}/envelope": {
      "post": {
        "operationId": "envelope",
        "summary": "Report errors from a Sentry SDK",
        "tags": [
          "Errors"
        ],
        "description": "The door for Sentry's open-source SDKs. An SDK is given an address made from an app key and Dokkit's host. Error events are stored; everything else is read and dropped, and the answer tells the SDK to stop sending those kinds. Gzip is accepted. The app key is read from the `X-Sentry-Auth` header, the `sentry_key` query value, or the envelope's first line. The `project` segment is whatever the SDK was given and is not used. Takes an app key (`dka_`).",
        "parameters": [
          {
            "name": "project",
            "in": "path",
            "description": "The project part of the address the SDK built.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "sentry_key",
            "in": "query",
            "description": "The app key, when the SDK sends it in the query.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "X-Sentry-Auth",
            "in": "header",
            "description": "Holds `sentry_key=<app key>` among its fields.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "Content-Encoding",
            "in": "header",
            "description": "Set to gzip when the body is compressed.",
            "schema": {
              "type": "string",
              "enum": [
                "gzip"
              ]
            },
            "required": false
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-sentry-envelope": {
              "schema": {
                "type": "string",
                "description": "A Sentry envelope: a header line, then item header and payload lines."
              },
              "example": "{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\",\"dsn\":\"https://dka_4Rm8@your.domain.com/1\"}\n{\"type\":\"event\"}\n{\"exception\":{\"values\":[{\"type\":\"TimeoutError\",\"value\":\"upload timed out\"}]},\"release\":\"2.3.0\"}\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted. The `X-Sentry-Rate-Limits` header tells the SDK to stop sending kinds Dokkit drops.",
            "headers": {
              "X-Sentry-Rate-Limits": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {}
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "413": {
            "description": "Too large.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_large",
                    "message": "Too large."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_gzip",
                    "message": "The body is not valid gzip."
                  }
                }
              }
            }
          }
        },
        "security": []
      },
      "options": {
        "operationId": "envelopePreflight",
        "summary": "Browser preflight for the envelope door",
        "tags": [
          "Errors"
        ],
        "description": "Answers a browser's preflight check with the CORS headers and no body. App keys are write-only and ship inside browser pages, so every origin is allowed. Takes no key.",
        "responses": {
          "204": {
            "description": "No content. The CORS headers are set."
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "project",
            "in": "path",
            "description": "The project part of the address the SDK built.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ]
      }
    },
    "/api/v1/program/{target}": {
      "get": {
        "operationId": "program",
        "summary": "Download the newer dokkit program",
        "tags": [
          "Work"
        ],
        "description": "The newer `dokkit` program this server holds, for a machine asked to fetch it. The target is the operating system and architecture, such as `windows-amd64`. Takes a machine key (`dkm_`).",
        "parameters": [
          {
            "name": "target",
            "in": "path",
            "description": "The kind of machine, as `<os>-<arch>`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+-[a-z0-9]+$"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "The program file.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "This server holds no program for that kind of machine."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/work": {
      "get": {
        "operationId": "work",
        "summary": "Ask for work",
        "tags": [
          "Work"
        ],
        "description": "The `dokkit` program asks for work and the server holds the request for up to `wait` seconds. 204 means nothing to do; ask again. The run's name is in `task`, and it is one of two. `dokkit:update` asks the program to fetch the newer copy the server holds. `dokkit:imessage` is offered only to the Mac chosen for iMessage and carries two more fields: `to`, the recipients, and `text`, the message. The program answers it on the result address with `exit_code` 0 when every recipient was sent to, 1 otherwise, or `refused` with the reason, and posts one line of output for each recipient. Takes a machine key (`dkm_`).",
        "parameters": [
          {
            "name": "wait",
            "in": "query",
            "description": "Seconds to hold the request.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "A run to do.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "task": {
                      "type": "string",
                      "enum": [
                        "dokkit:update",
                        "dokkit:imessage"
                      ],
                      "description": "What to do."
                    },
                    "to": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Only with `dokkit:imessage`: the phone numbers and email addresses to send to."
                    },
                    "text": {
                      "type": "string",
                      "description": "Only with `dokkit:imessage`: the message to send to each of them."
                    }
                  }
                },
                "example": {
                  "run": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "task": "dokkit:update"
                }
              }
            }
          },
          "204": {
            "description": "Nothing to do. Ask again."
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/work/{run}/output": {
      "post": {
        "operationId": "workOutput",
        "summary": "Send a chunk of output",
        "tags": [
          "Work"
        ],
        "description": "A chunk of the running run's output, as plain text. The server keeps the last part. Takes a machine key (`dkm_`).",
        "parameters": [
          {
            "name": "run",
            "in": "path",
            "description": "The run id from `GET /work`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "text/plain": {
              "schema": {
                "type": "string"
              },
              "example": "The newer copy is in place.\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "This machine is not running that."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/work/{run}/result": {
      "post": {
        "operationId": "workResult",
        "summary": "Send the result of a run",
        "tags": [
          "Work"
        ],
        "description": "Either an exit code with the duration, or a refusal with the reason. A refusal is how the program says it will not do the run. Takes a machine key (`dkm_`).",
        "parameters": [
          {
            "name": "run",
            "in": "path",
            "description": "The run id from `GET /work`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "exit_code": {
                    "type": "integer"
                  },
                  "duration_ms": {
                    "type": "integer"
                  },
                  "refused": {
                    "type": "string",
                    "description": "Why the run was not done."
                  }
                }
              },
              "example": {
                "exit_code": 0,
                "duration_ms": 81234
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "404": {
            "description": "The run is not running on this machine, or the body is neither an exit code nor a refusal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "This machine is not running that."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/day": {
      "get": {
        "operationId": "getDay",
        "summary": "The last day",
        "tags": [
          "Read"
        ],
        "description": "The last 24 hours in 48 half hours, for each machine, and the issues that opened in that time. The last cell is the half hour that holds `to`. A cell is `down` if an issue that was down overlapped it, else `degraded` if any issue overlapped it, else `quiet` if the machine was heard from, else `none` (no signal). An issue counts in the state it has now, for its whole life. Soon issues and issues of services that are not in production colour nothing. Takes a user token (`dku_`) with the `read` scope.",
        "parameters": [
          {
            "name": "to",
            "in": "query",
            "description": "The end of the window, RFC 3339. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time",
                      "description": "The start of the first cell."
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time",
                      "description": "The end of the last cell."
                    },
                    "machines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "cells": {
                            "type": "array",
                            "description": "48 states, oldest first.",
                            "items": {
                              "type": "string",
                              "enum": [
                                "quiet",
                                "degraded",
                                "down",
                                "none"
                              ]
                            }
                          }
                        }
                      }
                    },
                    "events": {
                      "type": "array",
                      "description": "Issues that opened in the window, oldest first.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "title": {
                            "type": "string"
                          },
                          "machine": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "degraded",
                              "down"
                            ]
                          },
                          "sent": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "What was done to tell someone: Paged, Pushed, Emailed, Sent an iMessage, Sent an SMS, Texted (an older alert) or Webhook called. It stays after the issue closes. Null if nothing was sent."
                          },
                          "closed_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "from": "2026-01-09T12:30:00Z",
                  "to": "2026-01-10T12:30:00Z",
                  "machines": [
                    {
                      "id": "6f1c2a3e-0b7d-4c55-9a11-2d3f4e5a6b70",
                      "name": "db-01",
                      "cells": [
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "down",
                        "down",
                        "quiet",
                        "quiet",
                        "quiet"
                      ]
                    },
                    {
                      "id": "0a9e8d7c-1b2a-4f33-8c44-5d6e7f809a1b",
                      "name": "web-02",
                      "cells": [
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "degraded",
                        "degraded"
                      ]
                    },
                    {
                      "id": "3c4d5e6f-7a8b-4c9d-a0b1-c2d3e4f50617",
                      "name": "nas",
                      "cells": [
                        "none",
                        "none",
                        "none",
                        "none",
                        "none",
                        "none",
                        "none",
                        "none",
                        "none",
                        "none",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet",
                        "quiet"
                      ]
                    }
                  ],
                  "events": [
                    {
                      "at": "2026-01-10T10:05:00Z",
                      "title": "db-01: Postgres is down",
                      "machine": "db-01",
                      "state": "down",
                      "sent": "Paged",
                      "closed_at": "2026-01-10T10:50:00Z"
                    },
                    {
                      "at": "2026-01-10T11:40:00Z",
                      "title": "web-02: Disk C is nearly full",
                      "machine": "web-02",
                      "state": "degraded",
                      "sent": "Pushed",
                      "closed_at": null
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "to is not an RFC 3339 time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/inbox": {
      "get": {
        "operationId": "inbox",
        "summary": "What needs a look",
        "tags": [
          "Read"
        ],
        "description": "What needs a look, what will soon, what was found and is not watched, what is snoozed, issues in other environments, and how many things are fine. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "needs_you": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Issue"
                      }
                    },
                    "soon": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Issue"
                      }
                    },
                    "not_watched": {
                      "type": "array",
                      "description": "What a scan found that nobody watches or dismissed.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "kind": {
                            "type": "string",
                            "description": "service or port."
                          },
                          "name": {
                            "type": "string"
                          },
                          "detail": {
                            "type": "object"
                          },
                          "machine": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "snoozed": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Issue"
                      }
                    },
                    "other_environments": {
                      "type": "array",
                      "description": "Issues of services that are not in production, by environment. They never count as needing you.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "environment": {
                            "type": "string"
                          },
                          "issues": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/Issue"
                            }
                          }
                        }
                      }
                    },
                    "fine": {
                      "type": "object",
                      "description": "Watched services with nothing open, and how many machines.",
                      "properties": {
                        "services": {
                          "type": "integer"
                        },
                        "machines": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "needs_you": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "short_id": "DOK-12",
                      "kind": "check",
                      "state": "down",
                      "title": "upload-portal is down",
                      "note": "Connection refused",
                      "count": 3,
                      "since": "2026-10-05T20:41:00Z",
                      "snoozed_until": null,
                      "acknowledged_at": null,
                      "machine": "web-02",
                      "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "alert": {
                        "channel": "page",
                        "status": "sent"
                      },
                      "soon": false
                    }
                  ],
                  "soon": [
                    {
                      "id": "9b2e4d10-77aa-4c3e-8f61-2a5c0d9e1b44",
                      "short_id": "DOK-13",
                      "kind": "check",
                      "state": "degraded",
                      "title": "Certificate for upload-portal expires in 12 days",
                      "note": "12 days left",
                      "count": 1,
                      "since": "2026-10-05T20:41:00Z",
                      "snoozed_until": null,
                      "acknowledged_at": null,
                      "machine": "web-02",
                      "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "alert": {
                        "channel": "push",
                        "status": "sent"
                      },
                      "soon": true
                    }
                  ],
                  "not_watched": [
                    {
                      "id": "c1d7a0f2-5e3b-4a96-b8d4-0f7e2a6c9b15",
                      "kind": "port",
                      "name": "5432",
                      "detail": {
                        "label": "Port 5432",
                        "saw": "postgres"
                      },
                      "machine": "db-01"
                    }
                  ],
                  "snoozed": [],
                  "other_environments": [
                    {
                      "environment": "dev",
                      "issues": []
                    }
                  ],
                  "fine": {
                    "services": 14,
                    "machines": 5
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/issues": {
      "get": {
        "operationId": "listIssues",
        "summary": "List issues",
        "tags": [
          "Read"
        ],
        "description": "Issues, newest first: the open and snoozed ones, or those of `state`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Issue"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "short_id": "DOK-12",
                      "kind": "check",
                      "state": "down",
                      "title": "upload-portal is down",
                      "note": "Connection refused",
                      "count": 3,
                      "since": "2026-10-05T20:41:00Z",
                      "snoozed_until": null,
                      "acknowledged_at": null,
                      "machine": "web-02",
                      "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "alert": {
                        "channel": "page",
                        "status": "sent"
                      }
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "open, snoozed, closed or all. Left out: open and snoozed.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "snoozed",
                "closed",
                "all"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "Only this service (id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "environment",
            "in": "query",
            "description": "Only in this environment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only from this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only up to this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      }
    },
    "/api/v1/issues/{id}": {
      "get": {
        "operationId": "getIssue",
        "summary": "Get one issue",
        "tags": [
          "Read"
        ],
        "description": "One issue, open or closed, with its notes. The id is the issue's uuid. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the issue.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Issue"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "closed_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "notes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "A note."
                          },
                          "description": "Left out without read-logs."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "short_id": "DOK-12",
                  "kind": "check",
                  "state": "down",
                  "title": "upload-portal is down",
                  "note": "Connection refused",
                  "count": 3,
                  "since": "2026-10-05T20:41:00Z",
                  "snoozed_until": null,
                  "acknowledged_at": null,
                  "machine": "web-02",
                  "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "alert": {
                    "channel": "page",
                    "status": "sent"
                  },
                  "closed_at": null,
                  "notes": [
                    {
                      "text": "Restarted the pool.",
                      "by": "token ci-bot"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/machines": {
      "get": {
        "operationId": "listMachines",
        "summary": "List machines",
        "tags": [
          "Read"
        ],
        "description": "Machines with their vitals. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Machine"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "db-01",
                      "kind": "enrolled",
                      "os": "windows",
                      "last_seen_at": "2026-10-05T21:14:03Z",
                      "down": false,
                      "needs_a_look": 0,
                      "vitals": {
                        "disk_used_percent": 61.2
                      }
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      }
    },
    "/api/v1/machines/{id}": {
      "get": {
        "operationId": "getMachine",
        "summary": "Get one machine",
        "tags": [
          "Read"
        ],
        "description": "One machine with its services and its thresholds. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Machine"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "services": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ServiceRow"
                          }
                        },
                        "thresholds": {
                          "type": "object",
                          "properties": {
                            "disk_used_percent": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "memory_used_percent": {
                              "type": [
                                "number",
                                "null"
                              ]
                            }
                          }
                        },
                        "metrics": {
                          "type": "array",
                          "description": "The latest reading of each of the machine's own numbers, however old.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "value": {
                                "type": "number"
                              },
                              "at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "silent": {
                          "type": "object",
                          "description": "How this machine going silent is announced; see PUT /machines/{id}/routing.",
                          "properties": {
                            "down": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "down_channels": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "night": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "db-01",
                  "kind": "enrolled",
                  "os": "windows",
                  "last_seen_at": "2026-10-05T21:14:03Z",
                  "down": false,
                  "needs_a_look": 0,
                  "vitals": {},
                  "services": [],
                  "thresholds": {
                    "disk_used_percent": 90,
                    "memory_used_percent": 90
                  },
                  "metrics": [
                    {
                      "name": "disk.used_percent.C",
                      "value": 71.4,
                      "at": "2026-10-05T21:14:03Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/machines/{id}/processes": {
      "get": {
        "operationId": "machineProcesses",
        "summary": "Get a machine's newest process list",
        "tags": [
          "Read"
        ],
        "description": "The newest list the machine sent, about once a minute. `count` is how many processes matched before `limit`. With nothing received yet: `at` null, `items` empty. Takes a user token (`dku_`) with the `read` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "sort",
            "in": "query",
            "description": "`memory` (default) or `cpu`.",
            "schema": {
              "type": "string",
              "enum": [
                "memory",
                "cpu"
              ]
            },
            "required": false
          },
          {
            "name": "q",
            "in": "query",
            "description": "Matches the name or the path, any case.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "pid",
            "in": "query",
            "description": "One process.",
            "schema": {
              "type": "integer"
            },
            "required": false
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many items.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "total_memory_bytes": {
                      "type": "integer",
                      "nullable": true
                    },
                    "count": {
                      "type": "integer"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "`pid`, `ppid`, `name`, `path`, `memory_bytes`, `memory_percent` (of `total_memory_bytes`), `cpu_percent` (null on a first sample), `session`, `user`."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such machine.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/machines/{id}/processes/history": {
      "get": {
        "operationId": "machineProcessHistory",
        "summary": "Get the top processes kept for a machine",
        "tags": [
          "Read"
        ],
        "description": "The top 10 by memory or by processor of every list received, oldest first, for up to 24 hours back. Takes a user token (`dku_`) with the `read` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "kind",
            "in": "query",
            "description": "`memory` (default) or `cpu`.",
            "schema": {
              "type": "string",
              "enum": [
                "memory",
                "cpu"
              ]
            },
            "required": false
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start, RFC 3339. Defaults to an hour before `to`; nothing older than 24 hours is kept.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "to",
            "in": "query",
            "description": "End, RFC 3339. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string"
                    },
                    "samples": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "`at`, and `items`: up to 10 of `pid`, `name`, `memory_bytes`, `cpu_percent`."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such machine.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A kind other than `memory` or `cpu`, a time that is not RFC 3339, or a `from` that is not before `to`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/machines/{id}/series": {
      "get": {
        "operationId": "machineSeries",
        "summary": "Get chart data for a machine's own numbers",
        "tags": [
          "Read"
        ],
        "description": "Without `metric`, the names of the metrics the machine reports itself (disks, memory, processor). With `metric`, its points between `from` and `to`. Answers 404 for an unknown machine, a time that is not RFC 3339, or a `from` that is not before `to`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer. Machine numbers are kept for 30 days.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "metric",
            "in": "query",
            "description": "The metric's name.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start, RFC 3339. Defaults to 24 hours before `to`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "to",
            "in": "query",
            "description": "End, RFC 3339. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "points",
            "in": "query",
            "description": "How many points.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 24
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "metrics": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "metric": {
                      "type": "string"
                    },
                    "points": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "One point: `at`, the start of its slice of time, and `value`, the average over it, or null when nothing was reported."
                      }
                    }
                  }
                },
                "example": {
                  "metric": "disk.used_percent.C",
                  "points": [
                    {
                      "at": "2026-10-05T20:00:00Z",
                      "value": 71.4
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/services": {
      "get": {
        "operationId": "listServices",
        "summary": "List services",
        "tags": [
          "Read"
        ],
        "description": "Services across machines. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Search the name and slug.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "machine",
            "in": "query",
            "description": "A machine id or name.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "service",
            "in": "query",
            "description": "Only this service (id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "environment",
            "in": "query",
            "description": "Only in this environment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "service, job, dependency or app.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceRow"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "slug": "upload-portal",
                      "name": "Upload portal",
                      "kind": "service",
                      "environment": "production",
                      "state": "ok",
                      "note": null,
                      "version": "2.3.0",
                      "held": null,
                      "machine": "web-02"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}": {
      "get": {
        "operationId": "getService",
        "summary": "Get one service",
        "tags": [
          "Read"
        ],
        "description": "One service with its checks, facts, routing, version and open issue. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [
                    {
                      "name": "front-page",
                      "type": "http",
                      "state": "ok",
                      "note": null,
                      "value": 38,
                      "checked_at": "2026-10-05T21:14:00Z",
                      "source": "file",
                      "config": {
                        "url": "http://localhost:8080/health"
                      }
                    }
                  ],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/series": {
      "get": {
        "operationId": "serviceSeries",
        "summary": "Get chart data for a service",
        "tags": [
          "Read"
        ],
        "description": "Without `metric`, the names of the metrics the service has. With `metric`, its points between `from` and `to`. Answers 404 for an unknown service, a time that is not RFC 3339, or a `from` that is not before `to`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "metric",
            "in": "query",
            "description": "The metric's name.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start, RFC 3339. Defaults to 24 hours before `to`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "to",
            "in": "query",
            "description": "End, RFC 3339. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "points",
            "in": "query",
            "description": "How many points.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 24
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "metrics": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "metric": {
                      "type": "string"
                    },
                    "points": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "One point: `at`, the start of its slice of time, and `value`, the average over it, or null when nothing was reported."
                      }
                    }
                  }
                },
                "example": {
                  "metric": "queue_depth",
                  "points": [
                    {
                      "at": "2026-10-05T20:00:00Z",
                      "value": 4
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/apps": {
      "post": {
        "operationId": "addApp",
        "summary": "Add an app that runs on many machines",
        "tags": [
          "Admin"
        ],
        "description": "Adds an app, such as a desktop program, that belongs to no one machine. Its key is returned once and goes into the app's build. Takes a user token (`dku_`) with the `admin` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "Label printer"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The app, with its key shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "5f0c1c0e-2a52-4c0f-9d5e-6f1b2f9a7c11",
                  "name": "Label printer",
                  "slug": "label-printer",
                  "key": "dka_..."
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_job",
                    "message": "A job needs a name and the machine it runs on."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/jobs": {
      "get": {
        "operationId": "listJobs",
        "summary": "List jobs",
        "tags": [
          "Read"
        ],
        "description": "Every job with its schedule, last run, last good run and next expected run. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Job"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "nightly-purge",
                      "machine": "nas",
                      "state": "ok",
                      "schedule": "0 2 * * *",
                      "last_run": {
                        "run": "r-2041",
                        "result": "ok",
                        "started_at": "2026-10-05T02:00:01Z",
                        "finished_at": "2026-10-05T02:01:22Z",
                        "exit_code": 0,
                        "duration_ms": 81234,
                        "tail": "Purged 412 files."
                      },
                      "last_good_run": null,
                      "next_expected": "2026-10-06T02:00:00Z",
                      "grace": "30m",
                      "max_runtime": null
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "Only this service (id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "environment",
            "in": "query",
            "description": "Only in this environment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "ok, degraded, down or unknown.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      },
      "post": {
        "operationId": "addJob",
        "summary": "Add a job to watch",
        "tags": [
          "Admin"
        ],
        "description": "Adds a job and its machine if it is new. The key and the addresses the job calls are returned once. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "machine"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "machine": {
                    "type": "string",
                    "description": "A machine name. A new name adds the machine."
                  },
                  "schedule": {
                    "type": "string",
                    "description": "A cron line, read as UTC."
                  },
                  "grace": {
                    "type": "string",
                    "description": "Such as 30m, 2h or 1d."
                  },
                  "max_runtime": {
                    "type": "string",
                    "description": "Such as 30m, 2h or 1d."
                  }
                }
              },
              "example": {
                "name": "Weekly report",
                "machine": "nas",
                "schedule": "0 2 * * 1",
                "grace": "20m"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The job, with its key shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAdded"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "Weekly report",
                  "key": "dka_3f9c0d2e7b4a",
                  "ping": {
                    "start": "https://your.domain.com/api/v1/in/dka_3f9c0d2e7b4a/job/weekly-report/start",
                    "ok": "https://your.domain.com/api/v1/in/dka_3f9c0d2e7b4a/job/weekly-report/ok",
                    "fail": "https://your.domain.com/api/v1/in/dka_3f9c0d2e7b4a/job/weekly-report/fail"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_job",
                    "message": "A job needs a name and the machine it runs on."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/jobs/{id}/runs": {
      "get": {
        "operationId": "listJobRuns",
        "summary": "List the runs of a job",
        "tags": [
          "Read"
        ],
        "description": "The recent runs of one job, up to 100. The id is the job's service id. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the job's service.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "result",
            "in": "query",
            "description": "running, ok or fail.",
            "schema": {
              "type": "string",
              "enum": [
                "running",
                "ok",
                "fail"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only from this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only up to this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JobRun"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "run": "r-2041",
                      "result": "ok",
                      "started_at": "2026-10-05T02:00:01Z",
                      "finished_at": "2026-10-05T02:01:22Z",
                      "exit_code": 0,
                      "duration_ms": 81234,
                      "tail": "Purged 412 files."
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/logs": {
      "get": {
        "operationId": "searchLogs",
        "summary": "Search log lines",
        "tags": [
          "Read"
        ],
        "description": "Log lines, newest first, at most 500. `level` is that level and worse. Answers 404 for an unknown machine or service, or a time that is not RFC 3339. Takes a user token (`dku_`) with the `read-logs` scope.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Search text.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "level",
            "in": "query",
            "description": "That level and worse.",
            "schema": {
              "type": "string",
              "enum": [
                "trace",
                "debug",
                "info",
                "warn",
                "error",
                "fatal"
              ]
            },
            "required": false
          },
          {
            "name": "machine",
            "in": "query",
            "description": "A machine id or name.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "service",
            "in": "query",
            "description": "A service id.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start, RFC 3339.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "to",
            "in": "query",
            "description": "End, RFC 3339.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "required": false
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LogLine"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "t": "2026-10-05T21:14:03.120Z",
                      "level": "error",
                      "msg": "Upload failed",
                      "fields": {
                        "route": "/upload",
                        "status": 500
                      },
                      "machine": "web-02",
                      "service": "upload-portal"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read-logs."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read-logs"
            ]
          }
        ]
      }
    },
    "/api/v1/errors/{id}": {
      "get": {
        "operationId": "getError",
        "summary": "Get one error group",
        "tags": [
          "Read"
        ],
        "description": "One group with the stack and tags of its latest report. The message is there only when the service keeps messages, and needs `read-logs`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer. `reports` lists the group's individual reports, newest first, as `{items, next}`; `cursor` and `limit` page them.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the error group.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorGroup"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "frames": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "One frame: file, function, line, own."
                          }
                        },
                        "tags": {
                          "type": "object",
                          "description": "Short labels."
                        },
                        "message": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "type": "TimeoutError",
                  "count": 14,
                  "first_seen": "2026-10-05T19:40:00Z",
                  "last_seen": "2026-10-05T21:14:03Z",
                  "handled": true,
                  "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "Upload portal",
                  "machine": "web-02",
                  "frames": [
                    {
                      "file": "app/worker.py",
                      "function": "run_job",
                      "line": 212,
                      "own": true
                    }
                  ],
                  "tags": {
                    "route": "/upload"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/versions": {
      "get": {
        "operationId": "listVersions",
        "summary": "Which version is where",
        "tags": [
          "Read"
        ],
        "description": "Which version of each service is on which machine, and under `apps`, each app that runs on many hosts with the hosts on each version. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "on": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "description": "One machine: machine, version, held, environment."
                            }
                          }
                        }
                      }
                    },
                    "apps": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "An app: id, slug, name, hosts and versions."
                      }
                    },
                    "next": {
                      "type": "null"
                    },
                    "program": {
                      "type": "object",
                      "description": "held is the program version this server gives machines to fetch, or null. machines lists what each machine runs.",
                      "properties": {
                        "held": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "machines": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "A machine: id, machine and version."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "slug": "upload-portal",
                      "name": "Upload portal",
                      "on": [
                        {
                          "machine": "web-02",
                          "version": "2.3.0",
                          "held": null,
                          "environment": "production"
                        }
                      ]
                    }
                  ],
                  "apps": [],
                  "next": null,
                  "program": {
                    "held": "0.2.0",
                    "machines": [
                      {
                        "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                        "machine": "web-02",
                        "version": "0.1.0"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/rules": {
      "get": {
        "operationId": "listRules",
        "summary": "List alert rules",
        "tags": [
          "Read"
        ],
        "description": "Alert rules, first match first. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Rule"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "kind": "job",
                      "state": "down",
                      "machine": "nas",
                      "channel": "email",
                      "on": true
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      },
      "post": {
        "operationId": "addRule",
        "summary": "Add an alert rule",
        "tags": [
          "Admin"
        ],
        "description": "Adds a rule to the end of the list. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RuleBody"
              },
              "example": {
                "kind": "job",
                "state": "down",
                "machine": "nas",
                "channel": "email"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new rule.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Rule"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "kind": "job",
                  "state": "down",
                  "machine": "nas",
                  "channel": "email",
                  "on": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/issues/{id}/snooze": {
      "post": {
        "operationId": "snoozeIssue",
        "summary": "Snooze an issue",
        "tags": [
          "Act"
        ],
        "description": "Snoozes an issue until a time. An unknown issue answers 404. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the issue.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "until"
                ],
                "properties": {
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "A time in the future, in RFC 3339."
                  }
                }
              },
              "example": {
                "until": "2026-10-06T08:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_time",
                    "message": "until must be a time in the future, in RFC 3339."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/issues/{id}/unsnooze": {
      "post": {
        "operationId": "unsnoozeIssue",
        "summary": "Bring a snoozed issue back",
        "tags": [
          "Act"
        ],
        "description": "Brings a snoozed issue back. An unknown issue answers 404. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the issue.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/issues/{id}/acknowledge": {
      "post": {
        "operationId": "acknowledgeIssue",
        "summary": "Mark an issue as seen",
        "tags": [
          "Act"
        ],
        "description": "Marks an issue as seen. An unknown issue answers 404. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the issue.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/issues/{id}/notes": {
      "post": {
        "operationId": "addIssueNote",
        "summary": "Add a note to an issue",
        "tags": [
          "Act"
        ],
        "description": "Adds a note. The note is signed with the token's label. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the issue.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "text": "Restarted the upload pool on web-02."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_note",
                    "message": "Needs text."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/issues/bulk": {
      "post": {
        "operationId": "bulkIssues",
        "summary": "Snooze, unsnooze or acknowledge many issues",
        "tags": [
          "Act"
        ],
        "description": "Does one action to a list of issue ids. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids",
                  "action"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "snooze",
                      "unsnooze",
                      "acknowledge"
                    ]
                  },
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Needed for snooze."
                  }
                }
              },
              "example": {
                "ids": [
                  "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3"
                ],
                "action": "snooze",
                "until": "2026-10-06T08:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_action",
                    "message": "action is snooze with until, unsnooze or acknowledge."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/watch": {
      "post": {
        "operationId": "watchService",
        "summary": "Start watching a found service",
        "tags": [
          "Act"
        ],
        "description": "Starts watching a found service. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/unwatch": {
      "post": {
        "operationId": "unwatchService",
        "summary": "Stop watching a service",
        "tags": [
          "Act"
        ],
        "description": "Stops watching a service. Open issues close and what the checks said is forgotten. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/routing": {
      "put": {
        "operationId": "setRouting",
        "summary": "Set who is told about a service",
        "tags": [
          "Act"
        ],
        "description": "Sets the channels for `down` and for `degraded`, each one channel or a list of channels that each send their own alert. A field that is not sent stays as it was; a value that is not a channel clears that route. `none` cannot be combined with other channels (422 `bad_routing`). `night` is optional; false holds the alerts of a night until it ends. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "down": {
                    "description": "One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "page",
                            "push",
                            "email",
                            "imessage",
                            "sms",
                            "digest",
                            "webhook",
                            "none"
                          ]
                        }
                      }
                    ]
                  },
                  "degraded": {
                    "description": "One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "page",
                            "push",
                            "email",
                            "imessage",
                            "sms",
                            "digest",
                            "webhook",
                            "none"
                          ]
                        }
                      }
                    ]
                  },
                  "night": {
                    "type": "boolean",
                    "description": "False holds this service's alerts at night until the night ends. Default true."
                  }
                }
              },
              "example": {
                "down": [
                  "page",
                  "email"
                ],
                "degraded": "digest",
                "night": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The routing now in force.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "down": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "degraded": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "night": {
                      "type": "boolean"
                    },
                    "down_channels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "degraded_channels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "down": "page,email",
                  "degraded": "digest",
                  "night": false,
                  "down_channels": [
                    "page",
                    "email"
                  ],
                  "degraded_channels": [
                    "digest"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`none` was combined with other channels.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_routing",
                    "message": "down: none cannot be combined with other channels."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/hold": {
      "put": {
        "operationId": "holdVersion",
        "summary": "Hold a version on purpose",
        "tags": [
          "Act"
        ],
        "description": "Marks a version as held on purpose. An empty or missing `version` clears the hold. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "version": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "version": "2.2.9"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/checks": {
      "put": {
        "operationId": "setChecks",
        "summary": "Replace the checks added in the interface",
        "tags": [
          "Admin"
        ],
        "description": "Replaces the checks added in the interface. Checks that came from a file are changed in the file. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "checks"
                ],
                "properties": {
                  "checks": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "type"
                      ],
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "http",
                            "tcp",
                            "process",
                            "windows-service",
                            "systemd"
                          ]
                        },
                        "config": {
                          "type": "object",
                          "description": "The check's settings."
                        },
                        "fail_after": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "checks": [
                  {
                    "name": "front-page",
                    "type": "http",
                    "config": {
                      "url": "http://localhost:8080/health"
                    },
                    "fail_after": 3
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_checks",
                    "message": "Needs checks, a list."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/restart": {
      "put": {
        "operationId": "setRestart",
        "summary": "Allow or stop automatic restart",
        "tags": [
          "Admin"
        ],
        "description": "Allows or stops automatic restart of a service. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "allow": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "allow": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/proposals/{id}/accept": {
      "post": {
        "operationId": "acceptProposal",
        "summary": "Accept a found thing",
        "tags": [
          "Act"
        ],
        "description": "Accepts a found service or port, so it is watched. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer. A hidden (system) proposal can be named by its id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the proposal.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/proposals/{id}/dismiss": {
      "post": {
        "operationId": "dismissProposal",
        "summary": "Dismiss a found thing",
        "tags": [
          "Act"
        ],
        "description": "Dismisses a found thing so it is not offered again. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer. A hidden (system) proposal can be named by its id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the proposal.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/rules/{id}": {
      "put": {
        "operationId": "updateRule",
        "summary": "Change an alert rule",
        "tags": [
          "Admin"
        ],
        "description": "Changes a rule: a field left out stays as it was and an empty one clears it. A kind, state or channel that is not one of its choices, null, or a machine that is not text answers 422. Its place in the order stays. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the rule.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RuleBody"
              },
              "example": {
                "kind": "job",
                "state": "down",
                "machine": "nas",
                "channel": "page",
                "on": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rule.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Rule"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "kind": "job",
                  "state": "down",
                  "machine": "nas",
                  "channel": "page",
                  "on": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "delete": {
        "operationId": "deleteRule",
        "summary": "Delete an alert rule",
        "tags": [
          "Admin"
        ],
        "description": "Deletes a rule. Deleting one that is not there still answers ok. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the rule.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/add-script": {
      "post": {
        "operationId": "addScript",
        "summary": "Make the script that adds a Windows machine",
        "tags": [
          "Admin"
        ],
        "description": "Makes a one-use enrolment token that lasts 24 hours and returns a PowerShell script holding it, the server address, the certificate fingerprint when one is needed, and the newest complete release. Run on the machine as administrator, the script fetches the program from this server with the token, checks the release signature and the file's checksum, enrols, installs the service and the updater, checks it reports and offers to delete itself. Takes a user token (`dku_`) with the `admin` scope.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What to call the machine; the token is labelled with it and the file named after it."
                  }
                }
              },
              "example": {
                "name": "reception"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key, shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "key": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "key": "dku_7Hq2pXz4Yc9VnB1aQw3E"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "kind is user with label and scopes, app with service, or enrol."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/keys": {
      "get": {
        "operationId": "listKeys",
        "summary": "List keys",
        "tags": [
          "Admin"
        ],
        "description": "Every key that still works, newest first. The key text is never shown again; `hint` is the start of it. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Key"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "kind": "user",
                      "hint": "dku_7Hq2",
                      "label": "ci-bot",
                      "scopes": [
                        "read",
                        "act"
                      ],
                      "used_at": null,
                      "expires_at": null,
                      "inserted_at": "2026-10-05T19:00:00Z",
                      "machine": null,
                      "service": null
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "user, app, enrol or machine.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Only on this machine (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only from this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only up to this time, RFC 3339.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      },
      "post": {
        "operationId": "addKey",
        "summary": "Make a key",
        "tags": [
          "Admin"
        ],
        "description": "Makes a user token with a label and scopes (only scopes the calling token holds, and never `admin`, which is made in Settings), an app key for a service, or a one-use enrolment token that lasts an hour. The key is shown once, in this answer. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "user",
                      "app",
                      "enrol"
                    ]
                  },
                  "label": {
                    "type": "string",
                    "description": "For user."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "read",
                        "read-logs",
                        "act",
                        "admin"
                      ]
                    },
                    "description": "For user."
                  },
                  "service": {
                    "type": "string",
                    "description": "A service id. For app."
                  },
                  "machine": {
                    "type": "string",
                    "description": "A machine name. For enrol, to let that machine enrol again."
                  }
                }
              },
              "example": {
                "kind": "user",
                "label": "ci-bot",
                "scopes": [
                  "read",
                  "act"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key, shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "key": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "key": "dku_7Hq2pXz4Yc9VnB1aQw3E"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "kind is user with label and scopes, app with service, or enrol."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/keys/{id}": {
      "delete": {
        "operationId": "revokeKey",
        "summary": "Revoke a key",
        "tags": [
          "Admin"
        ],
        "description": "Revokes a key. It stops working at once. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the key.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/phones": {
      "get": {
        "operationId": "listPhones",
        "summary": "List phones",
        "tags": [
          "Admin"
        ],
        "description": "The phones paired to read this server through its relay. Takes a user token (`dku_`) with the `admin` scope. Not offered through the MCP.",
        "responses": {
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "200": {
            "description": "The phones.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "added_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "last_fetch_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the relay last handed this phone its snapshot, as the relay told the last upload."
                          }
                        }
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "Work iPhone",
                      "added_at": "2026-10-13T09:00:00Z",
                      "last_fetch_at": null
                    }
                  ],
                  "next": null
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "post": {
        "operationId": "addPhone",
        "summary": "Pair a phone",
        "tags": [
          "Admin"
        ],
        "description": "Makes a phone with its own key and slot, and uploads at once. The answer holds `code`, the text a QR code carries, shown once: it holds the fetch token and the phone's key. The relay must be set first. Takes a user token (`dku_`) with the `admin` scope. Not offered through the MCP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Up to 60 characters."
                  }
                }
              },
              "example": {
                "name": "Work iPhone"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "200": {
            "description": "The phone, with its code.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "added_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "last_fetch_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "When the relay last handed this phone its snapshot, as the relay told the last upload."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "`dokkit-phone:1:` and base64url of the pairing JSON."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "Work iPhone",
                  "added_at": "2026-10-13T09:00:00Z",
                  "last_fetch_at": null,
                  "code": "dokkit-phone:1:eyJyZWxheSI6Imh0dHBzOi8vcmVsYXkuZXhhbXBsZS5jb20ifQ"
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_phone",
                    "message": "Set the relay first."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/phones/relay": {
      "get": {
        "operationId": "getPhoneRelay",
        "summary": "The relay",
        "tags": [
          "Admin"
        ],
        "description": "The relay phones are served through. The upload key is only set or not set. Takes a user token (`dku_`) with the `admin` scope. Not offered through the MCP.",
        "responses": {
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "200": {
            "description": "The relay.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "address": {
                      "type": "string"
                    },
                    "mailbox": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string",
                      "enum": [
                        "set",
                        "not set"
                      ],
                      "description": "The upload key is never shown."
                    },
                    "last_good_upload": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "last_error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The relay's answer to the last upload, while uploads are failing."
                    }
                  }
                },
                "example": {
                  "address": "https://relay.example.com",
                  "mailbox": "92bea4d52a1d53f0c8111edf39e26a89",
                  "key": "set",
                  "last_good_upload": "2026-10-13T09:01:00Z",
                  "last_error": null
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "put": {
        "operationId": "setPhoneRelay",
        "summary": "Set the relay",
        "tags": [
          "Admin"
        ],
        "description": "Saves the relay, then makes one upload and says whether the relay took it. `address` may end in `/m/<mailbox>`, as `dokkit relay add` prints it. A key left out stays. A different mailbox removes every phone; the same one keeps them. Takes a user token (`dku_`) with the `admin` scope. Not offered through the MCP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "address": {
                    "type": "string",
                    "description": "https only."
                  },
                  "mailbox": {
                    "type": "string",
                    "description": "32 lower-case hex characters."
                  },
                  "key": {
                    "type": "string",
                    "description": "The `dkr_` upload key."
                  }
                }
              },
              "example": {
                "address": "https://relay.example.com",
                "mailbox": "92bea4d52a1d53f0c8111edf39e26a89",
                "key": "dkr_..."
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "200": {
            "description": "The relay, and what the upload did.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "address": {
                          "type": "string"
                        },
                        "mailbox": {
                          "type": "string"
                        },
                        "key": {
                          "type": "string",
                          "enum": [
                            "set",
                            "not set"
                          ],
                          "description": "The upload key is never shown."
                        },
                        "last_good_upload": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The relay's answer to the last upload, while uploads are failing."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "uploaded": {
                          "type": "boolean"
                        },
                        "result": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "address": "https://relay.example.com",
                  "mailbox": "92bea4d52a1d53f0c8111edf39e26a89",
                  "key": "set",
                  "last_good_upload": "2026-10-13T09:01:00Z",
                  "last_error": null,
                  "uploaded": true,
                  "result": "The relay took the snapshot."
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_relay",
                    "message": "The address must start with https://."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/phones/{id}": {
      "delete": {
        "operationId": "removePhone",
        "summary": "Remove a phone",
        "tags": [
          "Admin"
        ],
        "description": "Removes a phone and uploads at once without its slot, so its token stops working. Takes a user token (`dku_`) with the `admin` scope. Not offered through the MCP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the phone.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "404": {
            "description": "No such phone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/users": {
      "get": {
        "operationId": "listUsers",
        "summary": "List people",
        "tags": [
          "Admin"
        ],
        "description": "The people who can sign in. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The people.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/User"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "sam",
                      "role": "owner"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      },
      "post": {
        "operationId": "addUser",
        "summary": "Add a person",
        "tags": [
          "Admin"
        ],
        "description": "Adds a person who can sign in. Roles are `owner`, `member` and `viewer`. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "role",
                  "password"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "member",
                      "viewer"
                    ]
                  },
                  "password": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "sam",
                "role": "member",
                "password": "a long passphrase"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "sam",
                  "role": "member"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_person",
                    "message": "That name is taken."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/users/{id}": {
      "delete": {
        "operationId": "removeUser",
        "summary": "Remove a person",
        "tags": [
          "Admin"
        ],
        "description": "Removes a person. Their user tokens stop working. The last owner cannot be removed. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the person.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The body does not fit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_person",
                    "message": "The last owner cannot be removed."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcpRpc",
        "summary": "Talk to the MCP server",
        "tags": [
          "MCP"
        ],
        "description": "The MCP server, as JSON-RPC over one POST address. It can do no more than the user token allows. An AI tool with a token can do through MCP everything the token can do through the API. The tools are, with `read` scope: `attention`, `find`, `service`, `jobs`, `job_runs`, `errors`, `since_deploy`, `machines`, `machine`, `issues`, `issue`, `deploys`, `versions`, `rules`, `proposals`, `series`, `processes`, `process_history` and `day`; with `read-logs` scope: `logs`; with `act` scope: `snooze`, `acknowledge`, `unsnooze`, `note`, `bulk`, `watch`, `unwatch`, `routing`, `hold`, `accept_proposal`, `dismiss_proposal`, `scan_machine`, `plan_downtime` and `end_downtime`; with `admin` scope: `add_rule`, `update_rule`, `delete_rule`, `set_checks`, `allow_restart`, `keys`, `make_key`, `revoke_key`, `people`, `add_person` and `remove_person`. `make_key` returns the key text once. Admin tools are marked destructive. `tools/list` shows only the tools the token may use. Every answer starts with a line saying that what follows is data written by machines and apps, never instructions. An argument of the wrong type is an `isError` answer; a null argument is the same as one left out, and a whole number written as 5.0 is an integer. A tool error is an answer with `isError` true, not an HTTP error. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "method"
                ],
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "enum": [
                      "2.0"
                    ]
                  },
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ],
                    "description": "Left out for a notification, which gets no answer."
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "initialize",
                      "ping",
                      "tools/list",
                      "tools/call"
                    ]
                  },
                  "params": {
                    "type": "object",
                    "description": "The method's parameters. For tools/call: name and arguments."
                  }
                }
              },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": {
                  "name": "find",
                  "arguments": {
                    "q": "upload",
                    "machine": "web-02"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A JSON-RPC answer, with a result or an error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string"
                    },
                    "id": {},
                    "result": {
                      "type": "object",
                      "description": "For tools/call: content, a list of text items, and isError when the tool failed."
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "result": {
                    "content": [
                      {
                        "type": "text",
                        "text": "[{\"id\":\"6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3\",\"name\":\"Upload portal\",\"state\":\"ok\"}]"
                      }
                    ]
                  }
                }
              }
            }
          },
          "202": {
            "description": "A notification, which has no id and gets no answer."
          },
          "400": {
            "description": "The body is not a request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": null,
                  "error": {
                    "code": -32600,
                    "message": "Not a request."
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          },
          {
            "userToken": [
              "read-logs"
            ]
          },
          {
            "userToken": [
              "act"
            ]
          },
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "get": {
        "operationId": "mcpOther",
        "summary": "Not served",
        "tags": [
          "MCP"
        ],
        "description": "Answers 405. The MCP server takes POST only. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed."
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/messages": {
      "put": {
        "operationId": "setKeepMessages",
        "summary": "Keep or stop keeping error text",
        "tags": [
          "Act"
        ],
        "description": "Keeps the text of a service's errors from now on, or stops keeping it. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the service.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "keep"
                ],
                "properties": {
                  "keep": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "keep": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "slug": "upload-portal",
                  "name": "Upload portal",
                  "kind": "service",
                  "environment": "production",
                  "state": "ok",
                  "note": null,
                  "version": "2.3.0",
                  "held": null,
                  "machine": "web-02",
                  "checks": [],
                  "routing": {
                    "down": "page",
                    "degraded": null
                  },
                  "restart": false,
                  "stats": null,
                  "stats_at": null,
                  "open_issue": null,
                  "keep_messages": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_keep",
                    "message": "keep is true or false."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/jobs/{id}/timing": {
      "put": {
        "operationId": "setJobTiming",
        "summary": "Set when a job is late",
        "tags": [
          "Act"
        ],
        "description": "Sets when a job counts as late (grace) and as run too long (max_runtime). A field left out stays as it was; an empty one clears it. A job from a dokkit.toml is changed in the file. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the job, which is its service id.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grace": {
                    "type": "string"
                  },
                  "max_runtime": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "grace": "45m",
                "max_runtime": "2h"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The job after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "nightly-purge",
                  "machine": "nas",
                  "state": "ok",
                  "schedule": "0 2 * * *",
                  "grace": "45m",
                  "max_runtime": "2h",
                  "last_run": null,
                  "last_good_run": null,
                  "next_expected": "2026-10-06T02:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_timing",
                    "message": "Write times as a number and a unit: 30m, 2h, 1d."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    },
    "/api/v1/machines/{id}/update": {
      "post": {
        "operationId": "updateProgram",
        "summary": "Approve a release for a machine, or ask it to fetch the program",
        "tags": [
          "Admin"
        ],
        "description": "With `version`, sets the machine's wanted version, and only to a version the server holds completely (422 `not_held`, or `bad_version` when it is not `v` and three numbers). The updater on the machine does the rest. Without a body, asks an enrolled machine to fetch the newer `dokkit` program from this server, which a person finishes on the machine; 422 when the server holds no program. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Asked: the version; or the run id of the fetch.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": "string"
                    },
                    "run": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Only for a fetch without a version."
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "version": "v1.2.3"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_program",
                    "message": "This server holds no program for machines to fetch."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version"
                ],
                "properties": {
                  "version": {
                    "type": "string",
                    "pattern": "^v\\d+\\.\\d+\\.\\d+$"
                  }
                }
              },
              "example": {
                "version": "v1.2.3"
              }
            }
          }
        }
      }
    },
    "/api/v1/alerts": {
      "get": {
        "operationId": "getAlertSettings",
        "summary": "Alert settings",
        "tags": [
          "Read"
        ],
        "description": "Who is told by default, the limits, the night and the mail settings. Addresses, user names, the Twilio account SID and secrets are shown only as `set` or `not set`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertSettings"
                },
                "example": {
                  "down": "page",
                  "degraded": "push",
                  "push_with": "ntfy",
                  "disk_limit": "90",
                  "memory_limit": "95",
                  "night_from": "22",
                  "night_to": "7",
                  "night_offset": "0",
                  "email_to": "oncall@example.com",
                  "pagerduty_key": "set",
                  "ntfy_url": "not set",
                  "webhook_url": "not set",
                  "smtp_password": "not set"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      },
      "put": {
        "operationId": "setAlertSettings",
        "summary": "Change alert settings",
        "tags": [
          "Admin"
        ],
        "description": "Saves the settings that are sent and checks them. `imessage_machine` must be an enrolled machine whose os is darwin, and every recipient in `imessage_to` and `sms_to` a phone number (or, for iMessage, an email address); otherwise 422. Answers with the settings as they now are. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertSettingsBody"
              },
              "example": {
                "down": "push",
                "pagerduty_key": "R0UT1NGKEY",
                "night_to": "8"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertSettings"
                },
                "example": {
                  "down": "push",
                  "degraded": "push",
                  "push_with": "ntfy",
                  "disk_limit": "90",
                  "memory_limit": "95",
                  "night_from": "22",
                  "night_to": "7",
                  "night_offset": "0",
                  "pagerduty_key": "set",
                  "webhook_url": "set"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_settings",
                    "message": "The night is whole hours: 0 to 23, and the hours from UTC from -12 to 14."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/alerts/test": {
      "post": {
        "operationId": "testAlert",
        "summary": "Send a test alert",
        "tags": [
          "Admin"
        ],
        "description": "Sends a test alert by one channel. Answers ok, or 422 with one reason. A page, and a push that goes through PagerDuty, stays open for two minutes and then resolves itself, so the phone has time to ring; the answer then carries `resolves_in` (120). For SMS it is Twilio's own message when it gave one, and for iMessage it is the Mac's answer, which is waited for up to a minute. For the others it is a fixed reason that never carries anything the far end said. A 422 with code `slow_down` comes after four tests at once for a token, then one every 25 seconds; a test that stays open costs twice as much: two at once, then one every 50 seconds. Answers 422 too when the channel is not one that sends, or `person` is not a person on the server (code `bad_person`). Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "channel"
                ],
                "properties": {
                  "channel": {
                    "type": "string",
                    "enum": [
                      "page",
                      "push",
                      "email",
                      "imessage",
                      "sms",
                      "digest",
                      "webhook"
                    ]
                  },
                  "person": {
                    "type": "string",
                    "description": "The id of a person. The test goes to that person's destination instead of the server-wide one."
                  }
                }
              },
              "example": {
                "channel": "webhook"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok",
                  "properties": {
                    "resolves_in": {
                      "type": "integer",
                      "description": "Seconds until a test that stays open resolves itself. Absent for the other channels."
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "send_failed",
                    "message": "The webhook has no address yet."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/alerts/suggestions": {
      "get": {
        "operationId": "listAlertSuggestions",
        "summary": "Alert suggestions",
        "tags": [
          "Read"
        ],
        "description": "Issues that opened six times or more in a month and are snoozed, where the daily digest could take them instead. Nothing changes until a person accepts. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AlertSuggestion"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "service": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "Flaky sync",
                      "state": "down",
                      "count": 6
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/alerts/suggestions/{id}/accept": {
      "post": {
        "operationId": "acceptAlertSuggestion",
        "summary": "Accept an alert suggestion",
        "tags": [
          "Admin"
        ],
        "description": "Sends that service's issues of that kind to the daily digest from now on. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the suggestion, from the list.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/alerts/suggestions/{id}/dismiss": {
      "post": {
        "operationId": "dismissAlertSuggestion",
        "summary": "Dismiss an alert suggestion",
        "tags": [
          "Admin"
        ],
        "description": "Dismisses the suggestion for that service. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the suggestion, from the list.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/logs/archives": {
      "get": {
        "operationId": "listArchives",
        "summary": "Archived months of logs",
        "tags": [
          "Read"
        ],
        "description": "The archived months of logs that can be loaded back. Takes a user token (`dku_`) with the `read-logs` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Archive"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "name": "logs-2025-01",
                      "size": 1048576,
                      "back_until": null
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read-logs."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read-logs"
            ]
          }
        ]
      }
    },
    "/api/v1/logs/retention": {
      "get": {
        "operationId": "getLogRetention",
        "summary": "How long log lines stay live",
        "tags": [
          "Admin"
        ],
        "description": "`months` is how many months stay live, `action` is what happens to an older month (`archive` or `delete`), and `older` is how many months are older than the limit now and will be handled by the next hourly sweep. Takes a user token (`dku_`) with the `admin` scope.",
        "responses": {
          "200": {
            "description": "The setting.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "months": {
                      "type": "integer"
                    },
                    "action": {
                      "type": "string",
                      "enum": [
                        "archive",
                        "delete"
                      ]
                    },
                    "older": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "months": 12,
                  "action": "archive",
                  "older": 0
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "put": {
        "operationId": "setLogRetention",
        "summary": "Change how long log lines stay live",
        "tags": [
          "Admin"
        ],
        "description": "`months` is a whole number from 1 to 120. With `action` `delete` an older month is dropped without a file, and `confirm` must be `true`. The change acts on the next sweep, not in this call. Archive files that already exist stay and can still be loaded back. A setting left out stays as it was. Takes a user token (`dku_`) with the `admin` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "months": {
                    "type": "integer"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "archive",
                      "delete"
                    ]
                  },
                  "confirm": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "months": 6,
                "action": "delete",
                "confirm": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The setting after the change, as in the GET.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "months": 6,
                  "action": "delete",
                  "older": 2
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "A setting is not right. The message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_settings",
                    "message": "months is a whole number from 1 to 120."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/logs/archives/{name}/restore": {
      "post": {
        "operationId": "restoreArchive",
        "summary": "Load an archived month back",
        "tags": [
          "Admin"
        ],
        "description": "Loads an archived month of logs back into the database for a week. Loading a month that is already back does nothing and answers 0 lines. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "The name of the month, from the list.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Loaded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "lines": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "lines": 18204
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/services/{id}/logs/move": {
      "post": {
        "operationId": "moveLogs",
        "summary": "Move log lines to another service",
        "tags": [
          "Admin"
        ],
        "description": "Moves the log lines of a service to another service, across all monthly partitions and in batches of 20,000, and answers when done. Lines can be picked by the value of one of their fields and by time. The moved lines take the target service's machine. With dry=true nothing changes and the answer counts the lines that would move. Takes a user token (`dku_`) with the `admin` scope. Not offered through MCP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The service the lines are taken from.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "dry",
            "in": "query",
            "description": "true to count without moving.",
            "schema": {
              "type": "boolean"
            },
            "required": false
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "The service the lines go to."
                  },
                  "field": {
                    "type": "string",
                    "description": "A field name in the line's fields: letters, digits and _."
                  },
                  "values": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 200,
                    "description": "Lines whose field is one of these move. Goes with field."
                  },
                  "from": {
                    "type": "string",
                    "description": "RFC 3339; lines at or after this time."
                  },
                  "until": {
                    "type": "string",
                    "description": "RFC 3339; lines before this time."
                  }
                },
                "required": [
                  "to"
                ]
              },
              "example": {
                "to": "7f0c2f0e-0000-4000-8000-000000000000",
                "field": "app_name",
                "values": [
                  "Records Finder"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved, or counted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "moved": {
                      "type": "integer"
                    },
                    "would_move": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "moved": 104211
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "Same service, bad field or values, or a bad time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_move",
                    "message": "to must be another service."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/services/{id}/logs/delete": {
      "post": {
        "operationId": "deleteLogs",
        "summary": "Delete log lines of a service",
        "tags": [
          "Admin"
        ],
        "description": "Deletes the log lines of a service that match every filter given, across all monthly partitions and in batches of 20,000, and answers when done. At least one filter is required, so a call can never empty a service by accident. With dry=true nothing changes and the answer counts the lines that would go. Takes a user token (`dku_`) with the `admin` scope. Not offered through MCP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The service the lines are deleted from.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "dry",
            "in": "query",
            "description": "true to count without deleting.",
            "schema": {
              "type": "boolean"
            },
            "required": false
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "field": {
                    "type": "string",
                    "description": "A field name in the line's fields: letters, digits and _."
                  },
                  "values": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 200,
                    "description": "Lines whose field is one of these go. Goes with field."
                  },
                  "q": {
                    "type": "string",
                    "description": "Lines whose message contains this text, case-insensitive."
                  },
                  "level": {
                    "type": "string",
                    "enum": [
                      "trace",
                      "debug",
                      "info",
                      "warn",
                      "error",
                      "fatal"
                    ],
                    "description": "Lines at exactly this level."
                  },
                  "from": {
                    "type": "string",
                    "description": "RFC 3339; lines at or after this time."
                  },
                  "until": {
                    "type": "string",
                    "description": "RFC 3339; lines before this time."
                  }
                }
              },
              "example": {
                "field": "app_name",
                "values": [
                  "Records Finder"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted, or counted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "integer"
                    },
                    "would_delete": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "deleted": 104211
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "No filter, a bad field, values, q or level, or a bad time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_filter",
                    "message": "Give at least one filter."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/releases": {
      "get": {
        "operationId": "listReleases",
        "summary": "List the releases held",
        "tags": [
          "Read"
        ],
        "description": "The versions this server holds for the updaters, newest first, each with its file names and whether it has both `SHA256SUMS` and `SHA256SUMS.sig`. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The releases.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Release"
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "version": "v1.2.3",
                      "files": [
                        "SHA256SUMS",
                        "SHA256SUMS.sig",
                        "dokkit-windows-x64",
                        "dokkit-windows-x64.zip"
                      ],
                      "complete": true
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Only what contains this text.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `next` of an earlier answer, for the following page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Items in a page: 1 to 500, 100 when left out (500 for logs).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ]
      }
    },
    "/api/v1/releases/{version}/{file}": {
      "put": {
        "operationId": "putReleaseFile",
        "summary": "Put a release file on the server",
        "tags": [
          "Admin"
        ],
        "description": "The body is the file, as is, up to 200 MB, and replaces a file of that name. It is written to disk as it arrives. The server stores it and does not check it; the updater does. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "version",
            "in": "path",
            "description": "A release version. Letters, digits, dots, dashes and underscores; not starting with a dot.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*$"
            },
            "required": true
          },
          {
            "name": "file",
            "in": "path",
            "description": "A file name in that release, such as `SHA256SUMS`. Same characters as a version.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*$"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "413": {
            "description": "The file is over 200 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_large",
                    "message": "A release file is at most 200 MB."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The name is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_name",
                    "message": "A name is letters, digits, dots, dashes and underscores only."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "get": {
        "operationId": "getReleaseFile",
        "summary": "Download a release file",
        "tags": [
          "Work"
        ],
        "description": "A file of a release, for the updater on a machine. Takes a machine key (`dkm_`).",
        "parameters": [
          {
            "name": "version",
            "in": "path",
            "description": "A release version. Letters, digits, dots, dashes and underscores; not starting with a dot.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*$"
            },
            "required": true
          },
          {
            "name": "file",
            "in": "path",
            "description": "A file name in that release, such as `SHA256SUMS`. Same characters as a version.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*$"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "The file.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/releases/{version}": {
      "delete": {
        "operationId": "deleteRelease",
        "summary": "Remove a release",
        "tags": [
          "Admin"
        ],
        "description": "Removes every file of that version from the server. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "version",
            "in": "path",
            "description": "A release version. Letters, digits, dots, dashes and underscores; not starting with a dot.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*$"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The name is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_name",
                    "message": "A name is letters, digits, dots, dashes and underscores only."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/update-all": {
      "post": {
        "operationId": "updateAll",
        "summary": "Ask every updater to move to a release",
        "tags": [
          "Admin"
        ],
        "description": "Sets the wanted version on every machine that has an updater. Only a version the server holds completely is accepted: 422 with code `not_held` otherwise, and `bad_version` when it is not `v` and three numbers. Nothing updates until this is called; there is no automatic following of new releases. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version"
                ],
                "properties": {
                  "version": {
                    "type": "string",
                    "pattern": "^v\\d+\\.\\d+\\.\\d+$"
                  }
                }
              },
              "example": {
                "version": "v1.2.3"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Asked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": "string"
                    },
                    "machines": {
                      "type": "integer",
                      "description": "How many machines were asked."
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "version": "v1.2.3",
                  "machines": 3
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The version is not one the server holds completely.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_held",
                    "message": "This server does not hold v1.2.3 completely: it needs SHA256SUMS and SHA256SUMS.sig."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/update": {
      "get": {
        "operationId": "askUpdate",
        "summary": "Ask which version to move to",
        "tags": [
          "Work"
        ],
        "description": "The updater on a machine tells the server what is installed and asks whether to move. The server remembers the versions, that the machine has an updater, and whether it hosts the server. 204 when no version is wanted, the server does not hold the wanted version completely, or every part the machine has is already on it; otherwise 200 with the version. Takes a machine key (`dkm_`).",
        "parameters": [
          {
            "name": "program",
            "in": "query",
            "description": "The installed program version.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "server",
            "in": "query",
            "description": "The installed server version. Left out when the machine does not run the server.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "updater",
            "in": "query",
            "description": "The installed updater version.",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "os",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "name": "arch",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Move to this version.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "version": "v1.2.3"
                }
              }
            }
          },
          "204": {
            "description": "Nothing to do."
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/update/result": {
      "post": {
        "operationId": "updateResult",
        "summary": "Report how an update went",
        "tags": [
          "Work"
        ],
        "description": "The server keeps the last result for the machine and clears its wanted version when `ok` is true. Takes a machine key (`dkm_`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version",
                  "ok"
                ],
                "properties": {
                  "version": {
                    "type": "string"
                  },
                  "ok": {
                    "type": "boolean"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "What was updated, or what went wrong."
                  }
                }
              },
              "example": {
                "version": "v1.2.3",
                "ok": true,
                "note": "program, updater"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_result",
                    "message": "Needs version, ok and at most 2000 characters of note."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/certificate": {
      "get": {
        "operationId": "getCertificate",
        "summary": "The certificates served",
        "tags": [
          "Read"
        ],
        "description": "The certificate the server was installed with and the one put in place with `PUT`, each `null` when there is none. Never a key. Takes a user token (`dku_`) with the `read` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The certificates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Certificates"
                },
                "example": {
                  "installed": {
                    "names": [
                      "web-01"
                    ],
                    "issuer": "web-01",
                    "not_after": "2036-10-01T13:00:00Z",
                    "self_signed": true,
                    "sha256": "a99f59d72d4fe3caf976fa019c671dc3e184b9f9c4e7b6ce4494e9c85df1ad3d"
                  },
                  "uploaded": {
                    "names": [
                      "example.dokkit.dev"
                    ],
                    "issuer": "R11",
                    "not_after": "2027-01-19T13:00:00Z",
                    "self_signed": false,
                    "sha256": "069ea4a89410f21e10658bbdb91aa9555af7e39294d4c08e999c7765ddf0d390"
                  },
                  "renewal": {
                    "at": "2026-12-20T03:00:00Z",
                    "ok": true,
                    "note": "Renewed, runs out 2027-03-20"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      },
      "put": {
        "operationId": "putCertificate",
        "summary": "Put a certificate in place",
        "tags": [
          "Admin"
        ],
        "description": "Checks that both parse, that the key belongs to the first certificate, that it is neither expired nor not yet valid and that it names at least one DNS name, then saves both and serves the certificate at once to clients that ask for one of its names (SNI). Anything else, and a client that connects by address, still gets the installed certificate. A server that does not serve TLS itself answers 422. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cert",
                  "key"
                ],
                "properties": {
                  "cert": {
                    "type": "string",
                    "description": "PEM: the leaf certificate first, then its chain."
                  },
                  "key": {
                    "type": "string",
                    "description": "PEM private key of the leaf certificate, not encrypted."
                  }
                }
              },
              "example": {
                "cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
                "key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The certificates after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Certificates"
                },
                "example": {
                  "installed": {
                    "names": [
                      "web-01"
                    ],
                    "issuer": "web-01",
                    "not_after": "2036-10-01T13:00:00Z",
                    "self_signed": true,
                    "sha256": "a99f59d72d4fe3caf976fa019c671dc3e184b9f9c4e7b6ce4494e9c85df1ad3d"
                  },
                  "uploaded": {
                    "names": [
                      "example.dokkit.dev"
                    ],
                    "issuer": "R11",
                    "not_after": "2027-01-19T13:00:00Z",
                    "self_signed": false,
                    "sha256": "069ea4a89410f21e10658bbdb91aa9555af7e39294d4c08e999c7765ddf0d390"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "A check failed. The message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_certificate",
                    "message": "The key does not belong to the certificate."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "delete": {
        "operationId": "deleteCertificate",
        "summary": "Remove the uploaded certificate",
        "tags": [
          "Admin"
        ],
        "description": "Removes the uploaded certificate. The installed one is not touched. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The certificates after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Certificates"
                },
                "example": {
                  "installed": {
                    "names": [
                      "web-01"
                    ],
                    "issuer": "web-01",
                    "not_after": "2036-10-01T13:00:00Z",
                    "self_signed": true,
                    "sha256": "a99f59d72d4fe3caf976fa019c671dc3e184b9f9c4e7b6ce4494e9c85df1ad3d"
                  },
                  "uploaded": {
                    "names": [
                      "example.dokkit.dev"
                    ],
                    "issuer": "R11",
                    "not_after": "2027-01-19T13:00:00Z",
                    "self_signed": false,
                    "sha256": "069ea4a89410f21e10658bbdb91aa9555af7e39294d4c08e999c7765ddf0d390"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "A check failed. The message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_certificate",
                    "message": "The key does not belong to the certificate."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/certificate/settings": {
      "get": {
        "operationId": "getCertificateSettings",
        "summary": "Settings for getting a certificate by itself",
        "tags": [
          "Admin"
        ],
        "description": "The name, the Cloudflare token (only as `set` or `not set`) and the certificate authority's directory. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CertificateSettings"
                },
                "example": {
                  "acme_name": "example.dokkit.dev",
                  "acme_cloudflare_token": "set",
                  "acme_directory": "https://acme-v02.api.letsencrypt.org/directory"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      },
      "put": {
        "operationId": "setCertificateSettings",
        "summary": "Change those settings",
        "tags": [
          "Admin"
        ],
        "description": "Once `acme_name` and `acme_cloudflare_token` are set, the server gets a certificate for the name from the certificate authority, proving the name with a TXT record at `_acme-challenge.<name>` that it adds at Cloudflare and removes again, and renews it when 30 days or fewer are left. A setting left out stays as it was. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CertificateSettingsBody"
              },
              "example": {
                "acme_name": "example.dokkit.dev",
                "acme_cloudflare_token": "cf-api-token"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CertificateSettings"
                },
                "example": {
                  "acme_name": "example.dokkit.dev",
                  "acme_cloudflare_token": "set",
                  "acme_directory": "https://acme-v02.api.letsencrypt.org/directory"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "A setting is not right. The message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_settings",
                    "message": "acme_name is a DNS name such as example.dokkit.dev, without a wildcard."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/certificate/renew": {
      "post": {
        "operationId": "renewCertificate",
        "summary": "Get a certificate now",
        "tags": [
          "Admin"
        ],
        "description": "Gets a certificate for `acme_name` now, whatever the one held has left. It waits for the whole exchange, which can take a few minutes. Takes a user token (`dku_`) with the `admin` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "responses": {
          "200": {
            "description": "The certificates after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Certificates"
                },
                "example": {
                  "installed": {
                    "names": [
                      "web-01"
                    ],
                    "issuer": "web-01",
                    "not_after": "2036-10-01T13:00:00Z",
                    "self_signed": true,
                    "sha256": "a99f59d72d4fe3caf976fa019c671dc3e184b9f9c4e7b6ce4494e9c85df1ad3d"
                  },
                  "uploaded": {
                    "names": [
                      "example.dokkit.dev"
                    ],
                    "issuer": "R11",
                    "not_after": "2027-01-19T13:00:00Z",
                    "self_signed": false,
                    "sha256": "069ea4a89410f21e10658bbdb91aa9555af7e39294d4c08e999c7765ddf0d390"
                  },
                  "renewal": {
                    "at": "2026-12-20T03:00:00Z",
                    "ok": true,
                    "note": "Renewed, runs out 2027-03-20"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "422": {
            "description": "It failed. The message says why; the same sentence is kept as `renewal` in `GET /certificate`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "renewal_failed",
                    "message": "Cloudflare refused: Authentication error"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/proposals/accept": {
      "post": {
        "operationId": "acceptProposals",
        "summary": "Accept found things",
        "tags": [
          "Act"
        ],
        "description": "Accepts some found services and ports (`ids`) or every one waiting and shown (`all` true; the operating system's own only with `system` true), so they are watched. Answers `done`, the ids acted on; `404` when none of them is waiting. Takes a user token (`dku_`) with the `act` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "all": {
                    "type": "boolean"
                  },
                  "system": {
                    "type": "boolean",
                    "description": "With all, also act on the operating system's own findings, which all leaves alone otherwise."
                  }
                }
              },
              "example": {
                "ids": [
                  "8f0f6f58-0000-0000-0000-000000000000"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/proposals/dismiss": {
      "post": {
        "operationId": "dismissProposals",
        "summary": "Dismiss found things",
        "tags": [
          "Act"
        ],
        "description": "Dismisses some proposals (`ids`) or every one waiting and shown (`all` true; the operating system's own only with `system` true). Answers `done`, the ids acted on; `404` when none of them is waiting. Takes a user token (`dku_`) with the `act` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "all": {
                    "type": "boolean"
                  },
                  "system": {
                    "type": "boolean",
                    "description": "With all, also act on the operating system's own findings, which all leaves alone otherwise."
                  }
                }
              },
              "example": {
                "ids": [
                  "8f0f6f58-0000-0000-0000-000000000000"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/dependencies": {
      "post": {
        "operationId": "addDependency",
        "summary": "Add something outside that is depended on",
        "tags": [
          "Admin"
        ],
        "description": "Adds a service of kind `dependency`. With `check` (`{type: http, url}` or `{type: tcp, address: host:port}`) it is put on `machine`, an enrolled machine whose dokkit runs the check; without a check it belongs to the group machine Outside. Answers as `GET /services/{id}` does. Takes a user token (`dku_`) with the `admin` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "machine": {
                    "type": "string"
                  },
                  "check": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "http",
                          "tcp"
                        ]
                      },
                      "url": {
                        "type": "string"
                      },
                      "address": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "name": "Payment gateway",
                "machine": "web-01",
                "check": {
                  "type": "http",
                  "url": "https://pay.example.com/health"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quiet": {
      "post": {
        "operationId": "quiet",
        "summary": "Say this machine is going down on purpose",
        "tags": [
          "Intake"
        ],
        "description": "A machine says it is going down on purpose for a while, such as for a restart. For that long from now, the machine going silent, and its services' and jobs' checks going down or missing, open no issue and send nothing. When the time is up the usual rules resume at once. A second call replaces the window; `for` of `0` ends it. Issues already open stay open. Takes a machine key (`dkm_`) and can only quiet the machine the key belongs to.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "for"
                ],
                "properties": {
                  "for": {
                    "type": "string",
                    "description": "How long, such as `90s`, `15m` or `4h`; at most `4h`. `0` ends the window."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why, at most 200 characters."
                  }
                }
              },
              "example": {
                "for": "15m",
                "reason": "nightly update"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done: when the window ends, or null when it was ended.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "until": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "until": "2026-10-12T03:20:00.000000Z"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key, or not a machine key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`for` is missing, not a duration, or more than 4 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_duration",
                    "message": "for is a duration up to 4h, such as 15m, or 0 to end it."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/machines/{id}/quiet": {
      "post": {
        "operationId": "planDowntime",
        "summary": "Plan downtime for a machine",
        "tags": [
          "Admin"
        ],
        "description": "The same as `POST /quiet`, for a person or a tool, for any enrolled machine. Answers with the machine's `id`, `name` and `planned_downtime` (`started_at`, `until`, `reason`, `by`), null when none is running. 404 when the machine is not an enrolled machine. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "for"
                ],
                "properties": {
                  "for": {
                    "type": "string",
                    "description": "How long, such as `15m` or `4h`; at most `4h`. `0` ends the window."
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "for": "30m",
                "reason": "disk swap"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "web01",
                  "planned_downtime": {
                    "started_at": "2026-10-12T03:00:00.000000Z",
                    "until": "2026-10-12T03:30:00.000000Z",
                    "ended_at": null,
                    "reason": "disk swap",
                    "by": "token ops",
                    "ended_by": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`for` is missing, not a duration, or more than 4 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_duration",
                    "message": "for is a duration up to 4h, such as 15m, or 0 to end it."
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "endDowntime",
        "summary": "End a machine's planned downtime",
        "tags": [
          "Admin"
        ],
        "description": "Ends the window now, and the usual rules apply from this moment. Answers like `POST`. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "web01",
                  "planned_downtime": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/machines/{id}/quiet/schedules": {
      "get": {
        "operationId": "listDowntimeSchedules",
        "summary": "A machine's scheduled planned downtime",
        "tags": [
          "Read"
        ],
        "description": "The planned downtime scheduled for the machine, as `{items, next}`. Each item has `id`, `repeat` (`once`, `daily` or `weekly`), `at` (for `once`), `time` (for the repeating kinds, in the server's night time zone, or UTC when none is set), `days` (1 for Monday to 7 for Sunday), `for_seconds`, `reason`, `by` and `next` (the next start, or null). Takes a user token (`dku_`) with the `read` scope. 404 when the machine is not an enrolled machine.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "The schedules.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "items": [
                    {
                      "id": "0b6f2d54-8a1e-4e0c-9d7b-2f3a61c4d8e5",
                      "repeat": "daily",
                      "at": null,
                      "time": "02:00",
                      "days": [1, 2, 3, 4, 5, 6, 7],
                      "for_seconds": 1800,
                      "reason": "nightly update",
                      "by": "ana",
                      "next": "2026-10-14T09:00:00.000000Z"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "scheduleDowntime",
        "summary": "Schedule planned downtime for a machine",
        "tags": [
          "Act"
        ],
        "description": "Schedules a window that the server starts by itself, once at a time or repeating. A window it starts is not ended early when the machine comes back; it ends when its time is up or when someone ends it. If the server was down at the start, the window covers the time that is left. Answers with the schedule. 404 when the machine is not an enrolled machine; 422 `bad_duration` or `bad_schedule` for a bad length, repeat, time, days or a `once` time in the past. Takes a user token (`dku_`) with the `act` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "repeat",
                  "for"
                ],
                "properties": {
                  "repeat": {
                    "type": "string",
                    "enum": [
                      "once",
                      "daily",
                      "weekly"
                    ]
                  },
                  "at": {
                    "type": "string",
                    "description": "For `once`: the start, in RFC 3339."
                  },
                  "time": {
                    "type": "string",
                    "description": "For `daily` and `weekly`: the time of day, such as `02:00`, in the server's night time zone (UTC when none is set)."
                  },
                  "days": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 7
                    },
                    "description": "For `weekly`: the weekdays, 1 for Monday to 7 for Sunday."
                  },
                  "for": {
                    "type": "string",
                    "description": "How long, such as `30m` or `4h`; at most `4h`."
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "repeat": "weekly",
                "time": "02:00",
                "days": [1, 3],
                "for": "30m",
                "reason": "nightly update"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The schedule.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "0b6f2d54-8a1e-4e0c-9d7b-2f3a61c4d8e5",
                  "repeat": "weekly",
                  "at": null,
                  "time": "02:00",
                  "days": [1, 3],
                  "for_seconds": 1800,
                  "reason": "nightly update",
                  "by": "token ops",
                  "next": "2026-10-14T09:00:00.000000Z"
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "A bad length, repeat, time or days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_schedule",
                    "message": "time is the time of day, such as 02:00."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/machines/{id}/quiet/schedules/{schedule}": {
      "delete": {
        "operationId": "removeDowntimeSchedule",
        "summary": "Remove a scheduled planned downtime",
        "tags": [
          "Act"
        ],
        "description": "Removes the schedule. A window it already started keeps running; end it with `DELETE /machines/{id}/quiet`. Takes a user token (`dku_`) with the `act` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "name": "schedule",
            "in": "path",
            "description": "The id of the schedule.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/machines/{id}/scan": {
      "post": {
        "operationId": "scanMachine",
        "summary": "Ask a machine to look itself over again",
        "tags": [
          "Admin"
        ],
        "description": "Queues the built-in piece of work `dokkit:scan` for an enrolled machine. It takes no parameters. The machine runs the same read-only scan as `dokkit scan` and sends the result to proposals, where nothing is watched until it is accepted. A program too old to know `dokkit:scan` refuses the run. 404 when the machine is not an enrolled machine. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Asked: the id of the queued run.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                },
                "example": {
                  "run": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The input is not right.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_program",
                    "message": "This server holds no program for machines to fetch."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version"
                ],
                "properties": {
                  "version": {
                    "type": "string",
                    "pattern": "^v\\d+\\.\\d+\\.\\d+$"
                  }
                }
              },
              "example": {
                "version": "v1.2.3"
              }
            }
          }
        }
      }
    },
    "/api/v1/records": {
      "get": {
        "operationId": "records",
        "summary": "Get the found jobs whose runs are read from the machine's own record",
        "tags": [
          "Intake"
        ],
        "description": "The accepted found jobs on this machine whose runs the `dokkit` program reads from the system's own record (a Windows Task Scheduler task or a systemd timer) and reports, with no change to the job. `source` is `task-scheduler` or `systemd-timer`; `unit` is the task's path or the timer's name. Takes a machine key (`dkm_`).",
        "responses": {
          "200": {
            "description": "The jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "service": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string"
                          },
                          "unit": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "service": "nightly-export",
                      "name": "nightly-export",
                      "source": "task-scheduler",
                      "unit": "\\Nightly export"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Needs a machine key."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "machineKey": []
          }
        ]
      }
    },
    "/api/v1/alerts/people": {
      "get": {
        "operationId": "listAlertPeople",
        "summary": "People alerts can be told",
        "tags": [
          "Read"
        ],
        "description": "Each person on the server with the destinations they have of their own. The ntfy address, the Pushover user key and the PagerDuty routing key are only `set` or `not set`. Takes a user token (`dku_`) with the `read` scope.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string"
                          },
                          "email": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "phone": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "imessage": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "ntfy_url": {
                            "type": "string",
                            "enum": [
                              "set",
                              "not set"
                            ]
                          },
                          "pushover_user": {
                            "type": "string",
                            "enum": [
                              "set",
                              "not set"
                            ]
                          },
                          "pagerduty_key": {
                            "type": "string",
                            "enum": [
                              "set",
                              "not set"
                            ]
                          }
                        }
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "name": "ada",
                      "role": "member",
                      "email": "ada@example.com",
                      "phone": "+15555550100",
                      "imessage": null,
                      "ntfy_url": "set",
                      "pushover_user": "not set",
                      "pagerduty_key": "not set"
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      }
    },
    "/api/v1/alerts/who": {
      "get": {
        "operationId": "listAlertWho",
        "summary": "Who is told at each level",
        "tags": [
          "Read"
        ],
        "description": "Where someone is named: the default, a rule, a service or a check. The most specific level that names someone decides: the check, then the service, then the rule, then the default. A level that names nobody is left to the level above, and the default is everyone. Takes a user token (`dku_`) with the `read` scope.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "scope": {
                            "type": "string",
                            "enum": [
                              "default",
                              "rule",
                              "service",
                              "check"
                            ]
                          },
                          "id": {
                            "type": "string",
                            "description": "The rule, service or check. Empty for the default."
                          },
                          "to": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Person ids, or [\"everyone\"] for the server-wide destinations."
                          }
                        }
                      }
                    },
                    "next": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Always null for now. Lists are not paged yet."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "scope": "service",
                      "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                      "to": [
                        "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f4"
                      ]
                    }
                  ],
                  "next": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not read."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "read"
            ]
          }
        ]
      },
      "put": {
        "operationId": "setAlertWho",
        "summary": "Name who is told at one level",
        "tags": [
          "Admin"
        ],
        "description": "`to` is a list of person ids, or `[\"everyone\"]`, which is the server-wide destinations and behaves as before. An empty list names nobody at that level, so the level above decides. Takes a user token (`dku_`) with the `admin` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "type": "string",
                    "enum": [
                      "default",
                      "rule",
                      "service",
                      "check"
                    ]
                  },
                  "id": {
                    "type": "string"
                  },
                  "to": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Person ids, or [\"everyone\"] for the server-wide destinations."
                  },
                  "service": {
                    "type": "string",
                    "description": "For a check: the id of its service, sent with `name` instead of `id`."
                  },
                  "name": {
                    "type": "string",
                    "description": "For a check: its name."
                  }
                },
                "required": [
                  "scope",
                  "to"
                ]
              },
              "example": {
                "scope": "service",
                "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                "to": [
                  "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f4"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scope": {
                      "type": "string",
                      "enum": [
                        "default",
                        "rule",
                        "service",
                        "check"
                      ]
                    },
                    "id": {
                      "type": "string",
                      "description": "The rule, service or check. Empty for the default."
                    },
                    "to": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Person ids, or [\"everyone\"] for the server-wide destinations."
                    }
                  }
                },
                "example": {
                  "scope": "service",
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "to": [
                    "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f4"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "Refused, with the reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/alerts/people/{id}": {
      "put": {
        "operationId": "updateAlertPerson",
        "summary": "Change a person's destinations",
        "tags": [
          "Admin"
        ],
        "description": "A destination left out stays as it was; an empty one clears it. Email and iMessage may hold several addresses with commas between them. The ntfy address must be https. A person's ntfy topic gets the server's ntfy token only when it is on the same host as the server's ntfy address. A person with no destination for a channel makes that alert fail with a reason that names them. Takes a user token (`dku_`) with the `admin` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the person.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  },
                  "imessage": {
                    "type": "string"
                  },
                  "ntfy_url": {
                    "type": "string"
                  },
                  "pushover_user": {
                    "type": "string"
                  },
                  "pagerduty_key": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "email": "ada@example.com",
                "phone": "+15555550100"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string"
                    },
                    "email": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "phone": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "imessage": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "ntfy_url": {
                      "type": "string",
                      "enum": [
                        "set",
                        "not set"
                      ]
                    },
                    "pushover_user": {
                      "type": "string",
                      "enum": [
                        "set",
                        "not set"
                      ]
                    },
                    "pagerduty_key": {
                      "type": "string",
                      "enum": [
                        "set",
                        "not set"
                      ]
                    }
                  }
                },
                "example": {
                  "id": "6f1c2a8e-3b7d-4c52-9e0a-5d41b7c8e9f3",
                  "name": "ada",
                  "role": "member",
                  "email": "ada@example.com",
                  "phone": "+15555550100",
                  "imessage": null,
                  "ntfy_url": "set",
                  "pushover_user": "not set",
                  "pagerduty_key": "not set"
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not admin."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "Refused, with the reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "admin"
            ]
          }
        ]
      }
    },
    "/api/v1/machines/{id}/routing": {
      "put": {
        "operationId": "setMachineRouting",
        "summary": "Set how a machine that stops reporting is announced",
        "tags": [
          "Act"
        ],
        "description": "Sets the channels used when this machine stops reporting (`down`: one channel or a list; empty uses the server-wide choice for down) and whether the alert may wake someone at night (`night`, default true; false holds it until the night ends). A field that is not sent stays as it was. Takes a user token (`dku_`) with the `act` scope. Without `read-logs`, job output, error messages and notes are left out of the answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The id of the machine.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "down": {
                    "description": "One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "page",
                            "push",
                            "email",
                            "imessage",
                            "sms",
                            "digest",
                            "webhook",
                            "none"
                          ]
                        }
                      }
                    ]
                  },
                  "night": {
                    "type": "boolean",
                    "description": "False holds the alert for a silent machine at night until the night ends. Default true."
                  }
                }
              },
              "example": {
                "down": [
                  "page"
                ],
                "night": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The routing now in force.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "down": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "down_channels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "night": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "down": "page",
                  "down_channels": [
                    "page"
                  ],
                  "night": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or bad key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_key",
                    "message": "Missing or bad key."
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key may not do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "no_scope",
                    "message": "This token may not act."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "No such thing."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`none` was combined with other channels.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "bad_routing",
                    "message": "down: none cannot be combined with other channels."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "userToken": [
              "act"
            ]
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "enrolmentToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "An enrolment token, prefix `dke_`. One use, expires. Sent in the body of `POST /api/v1/enrol`, where it is traded for a machine key."
      },
      "machineKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A machine key, prefix `dkm_`. Held by the `dokkit` program. Reports for that machine, proposes, takes work."
      },
      "appKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An app key, prefix `dka_`. Held by a library or a script inside an app. Write-only, for one service, and safe inside a browser page."
      },
      "userToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "A user token, prefix `dku_`, for scripts and the MCP. Scopes: `read`, `read-logs`, `act`, `admin`. Reading needs `read`, log contents need `read-logs`, keys and people and rules and alert settings and interface-added checks and restart and making a job and updating the program and loading logs back need `admin`, and anything else that changes something needs `act`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        }
      },
      "Issue": {
        "type": "object",
        "description": "One issue, with the machine and service it is about.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "short_id": {
            "type": "string",
            "description": "The short id, such as DOK-12."
          },
          "kind": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "soon": {
            "type": "boolean",
            "description": "True for a warning ahead of a failure. It lists under Soon and never pages: if its route is page, it is sent as a push."
          },
          "title": {
            "type": "string"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "count": {
            "type": "integer"
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "snoozed_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "acknowledged_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "machine": {
            "type": "string",
            "description": "The machine's name."
          },
          "service": {
            "type": [
              "string",
              "null"
            ],
            "description": "The service id."
          },
          "alert": {
            "type": [
              "object",
              "null"
            ],
            "description": "The first of its alerts (channel and status). `alerts` lists all of them.",
            "properties": {
              "channel": {
                "type": "string"
              },
              "status": {
                "type": "string"
              }
            }
          },
          "alerts": {
            "type": "array",
            "description": "Every alert sent for it, one for each channel: channel and status.",
            "items": {
              "type": "object",
              "properties": {
                "channel": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ServiceRow": {
        "type": "object",
        "description": "A service as it appears in a list.",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "service",
              "job",
              "dependency",
              "app"
            ]
          },
          "environment": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "description": "ok, degraded, down, unknown or unwatched."
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": [
              "string",
              "null"
            ]
          },
          "held": {
            "type": [
              "string",
              "null"
            ],
            "description": "The version held on purpose, if any."
          },
          "machine": {
            "type": "string",
            "description": "The machine's name."
          }
        }
      },
      "Service": {
        "type": "object",
        "description": "One service with its checks, routing, restart setting, stats and open issue, on top of the list fields.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ServiceRow"
          },
          {
            "type": "object",
            "properties": {
              "checks": {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "A check: name, type, state, note, value, checked_at, source and config."
                }
              },
              "routing": {
                "type": "object",
                "properties": {
                  "down": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "degraded": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "night": {
                    "type": "boolean"
                  },
                  "down_channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "degraded_channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "restart": {
                "type": "boolean"
              },
              "stats": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "The latest stats document."
              },
              "stats_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "open_issue": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Issue"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "keep_messages": {
                "type": "boolean",
                "description": "Whether the text of its errors is kept."
              }
            }
          }
        ]
      },
      "Machine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "enrolled, seen or group."
          },
          "os": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "down": {
            "type": "boolean"
          },
          "needs_a_look": {
            "type": "integer"
          },
          "vitals": {
            "type": "object",
            "description": "The latest machine-wide numbers."
          }
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The service id."
          },
          "name": {
            "type": "string"
          },
          "machine": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "schedule": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_run": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/JobRun"
              },
              {
                "type": "null"
              }
            ]
          },
          "last_good_run": {
            "type": [
              "object",
              "null"
            ],
            "description": "The last run that finished well."
          },
          "next_expected": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "grace": {
            "type": [
              "string",
              "null"
            ],
            "description": "How long past its time a run may start before it counts as missed."
          },
          "max_runtime": {
            "type": [
              "string",
              "null"
            ],
            "description": "How long a run that reported its start may take."
          }
        }
      },
      "JobRun": {
        "type": "object",
        "properties": {
          "run": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "description": "running, ok or fail."
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "exit_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "tail": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last lines of output, at most 16 KB. Needs read-logs."
          }
        }
      },
      "Deploy": {
        "type": "object",
        "properties": {
          "service": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "machine": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "from": {
            "type": [
              "string",
              "null"
            ]
          },
          "commit": {
            "type": [
              "string",
              "null"
            ]
          },
          "by": {
            "type": [
              "string",
              "null"
            ]
          },
          "how": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ErrorGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string"
          },
          "top": {
            "type": "object",
            "description": "The top frame of the stack."
          },
          "count": {
            "type": "integer"
          },
          "first_seen": {
            "type": "string",
            "format": "date-time"
          },
          "last_seen": {
            "type": "string",
            "format": "date-time"
          },
          "release": {
            "type": [
              "string",
              "null"
            ]
          },
          "host": {
            "type": [
              "string",
              "null"
            ]
          },
          "handled": {
            "type": "boolean"
          },
          "service": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "machine": {
            "type": "string"
          }
        }
      },
      "Rule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "",
              "job",
              "check",
              "machine"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "",
              "down",
              "degraded"
            ]
          },
          "machine": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "description": "The channel, or several with commas between."
          },
          "on": {
            "type": "boolean"
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "page",
                "push",
                "email",
                "imessage",
                "sms",
                "digest",
                "webhook",
                "none"
              ]
            },
            "description": "The same choice as a list. Either field may be sent; `channels` wins."
          }
        }
      },
      "RuleBody": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "",
              "job",
              "check",
              "machine"
            ],
            "description": "Empty means anything."
          },
          "state": {
            "type": "string",
            "enum": [
              "",
              "down",
              "degraded"
            ],
            "description": "Empty means down or degraded."
          },
          "machine": {
            "type": "string",
            "description": "A machine name. Empty means any."
          },
          "channel": {
            "description": "One channel as text, or several as text with commas between (stored and answered that way). Each sends its own alert; `none` stands alone.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "page",
                    "push",
                    "email",
                    "imessage",
                    "sms",
                    "digest",
                    "webhook",
                    "none"
                  ]
                }
              }
            ]
          },
          "on": {
            "type": "boolean",
            "description": "On update only. False turns the rule off."
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "page",
                "push",
                "email",
                "imessage",
                "sms",
                "digest",
                "webhook",
                "none"
              ]
            },
            "description": "The same choice as a list. Either field may be sent; `channels` wins."
          }
        }
      },
      "Key": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          },
          "hint": {
            "type": "string"
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "inserted_at": {
            "type": "string",
            "format": "date-time"
          },
          "machine": {
            "type": [
              "string",
              "null"
            ]
          },
          "service": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member",
              "viewer"
            ]
          }
        }
      },
      "Proposal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "description": "service, port, job, container or site."
          },
          "name": {
            "type": "string"
          },
          "detail": {
            "type": "object",
            "description": "What the scan saw. Found things only."
          },
          "machine": {
            "type": "string"
          },
          "origin": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "system",
              "software",
              "yours",
              null
            ],
            "description": "system ships with the operating system, software was installed by other software, yours was set up by a person. Null when the machine's program did not say; such a finding is shown and never hidden."
          },
          "why": {
            "type": [
              "string",
              "null"
            ],
            "description": "The fact the origin was decided from, in one line."
          }
        }
      },
      "LogLine": {
        "type": "object",
        "properties": {
          "t": {
            "type": "string",
            "format": "date-time"
          },
          "level": {
            "type": "string",
            "enum": [
              "trace",
              "debug",
              "info",
              "warn",
              "error",
              "fatal"
            ]
          },
          "msg": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "description": "Anything else on the line, as a flat object."
          },
          "machine": {
            "type": "string"
          },
          "service": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Refusal": {
        "type": "object",
        "properties": {
          "line": {
            "type": "integer"
          },
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "JobAdded": {
        "type": "object",
        "description": "A job that was added. The key and the addresses hold it, and are shown only now.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The service id."
          },
          "name": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "description": "The app key of the job, prefix dka_."
          },
          "ping": {
            "type": "object",
            "description": "The addresses the job calls: start, ok and fail.",
            "properties": {
              "start": {
                "type": "string"
              },
              "ok": {
                "type": "string"
              },
              "fail": {
                "type": "string"
              }
            }
          }
        }
      },
      "AlertSettings": {
        "type": "object",
        "description": "The alert settings. Addresses, user names and secrets are never returned: each is `set` or `not set`. A save that cleared a secret because its address changed adds a `note` that says so.",
        "properties": {
          "note": {
            "type": "string"
          },
          "down": {
            "type": "string",
            "description": "One channel, or several with commas between. One channel, or several with commas between."
          },
          "degraded": {
            "type": "string",
            "description": "One channel, or several with commas between. One channel, or several with commas between."
          },
          "push_with": {
            "type": "string",
            "enum": [
              "ntfy",
              "pushover",
              "pagerduty"
            ]
          },
          "disk_limit": {
            "type": "string"
          },
          "memory_limit": {
            "type": "string"
          },
          "night_from": {
            "type": "string"
          },
          "night_to": {
            "type": "string"
          },
          "night_offset": {
            "type": "string",
            "description": "Whole hours from UTC. Used only while night_zone is empty."
          },
          "digest_at": {
            "type": "string",
            "description": "The time of the daily digest, on the clock of night_zone (UTC while it is empty)."
          },
          "smtp_host": {
            "type": "string"
          },
          "smtp_port": {
            "type": "string"
          },
          "mail_from": {
            "type": "string"
          },
          "email_to": {
            "type": "string"
          },
          "imessage_machine": {
            "type": "string",
            "description": "The id of the enrolled Mac, whose os is darwin, that sends iMessages."
          },
          "imessage_to": {
            "type": "string",
            "description": "Who iMessages go to: phone numbers or email addresses, with commas between them."
          },
          "twilio_from": {
            "type": "string",
            "description": "The Twilio phone number SMS is sent from."
          },
          "sms_to": {
            "type": "string",
            "description": "Who SMS goes to: phone numbers, with commas between them."
          },
          "twilio_sid": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "twilio_token": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "pagerduty_key": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "ntfy_token": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "pushover_token": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "pushover_user": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "smtp_password": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "ntfy_url": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "webhook_url": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "heartbeat_url": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "smtp_user": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ]
          },
          "self_down": {
            "type": "string",
            "description": "The channel for Dokkit's own row when it stops taking reports or cannot reach its database. Empty is the loudest channel that is set up. One channel, or several with commas between. One channel, or several with commas between."
          },
          "self_degraded": {
            "type": "string",
            "description": "The channel for lesser trouble with Dokkit itself. Empty is push. One channel, or several with commas between. One channel, or several with commas between."
          },
          "imessage_status": {
            "type": "string",
            "description": "Where iMessage sending stands: set up once the chosen Mac has answered a test, else what is still missing."
          },
          "down_channels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same choice as a list."
          },
          "degraded_channels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same choice as a list."
          },
          "self_down_channels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same choice as a list."
          },
          "self_degraded_channels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same choice as a list."
          },
          "night_zone": {
            "type": "string",
            "description": "A time zone name such as America/Los_Angeles, in which the night and the digest time are kept. While it is empty the night is read in UTC plus night_offset."
          }
        }
      },
      "AlertSettingsBody": {
        "type": "object",
        "description": "Any of the settings. One that is left out stays as it was. A secret, an address or a user name is written here and never returned. A secret sent as an empty string is cleared; one left out keeps its value. A secret goes to one host: if ntfy_url or smtp_host changes and ntfy_token or smtp_password is not sent in the same call, it is cleared and the answer carries a `note` that says so. Addresses must be https and may not be this machine or a link-local address unless the server sets DOKKIT_ALLOW_LOCAL_ALERTS=1. smtp_port is 25, 465, 587 or 2525. The night is whole hours: night_from and night_to from 0 to 23, night_offset (hours from UTC) from -12 to 14.",
        "properties": {
          "down": {
            "description": "One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity. One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "page",
                    "push",
                    "email",
                    "imessage",
                    "sms",
                    "digest",
                    "webhook",
                    "none"
                  ]
                }
              }
            ]
          },
          "degraded": {
            "description": "One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity. One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "page",
                    "push",
                    "email",
                    "imessage",
                    "sms",
                    "digest",
                    "webhook",
                    "none"
                  ]
                }
              }
            ]
          },
          "push_with": {
            "type": "string",
            "enum": [
              "ntfy",
              "pushover",
              "pagerduty"
            ]
          },
          "disk_limit": {
            "type": "string"
          },
          "memory_limit": {
            "type": "string"
          },
          "night_from": {
            "type": "string"
          },
          "night_to": {
            "type": "string"
          },
          "night_offset": {
            "type": "string",
            "description": "Whole hours from UTC. Used only while night_zone is empty."
          },
          "digest_at": {
            "type": "string",
            "description": "The time of the daily digest, on the clock of night_zone (UTC while it is empty)."
          },
          "smtp_host": {
            "type": "string"
          },
          "smtp_port": {
            "type": "string"
          },
          "mail_from": {
            "type": "string"
          },
          "email_to": {
            "type": "string"
          },
          "imessage_machine": {
            "type": "string",
            "description": "The id of the enrolled Mac, whose os is darwin, that sends iMessages."
          },
          "imessage_to": {
            "type": "string",
            "description": "Who iMessages go to: phone numbers or email addresses, with commas between them."
          },
          "twilio_from": {
            "type": "string",
            "description": "The Twilio phone number SMS is sent from."
          },
          "sms_to": {
            "type": "string",
            "description": "Who SMS goes to: phone numbers, with commas between them."
          },
          "twilio_sid": {
            "type": "string"
          },
          "twilio_token": {
            "type": "string"
          },
          "pagerduty_key": {
            "type": "string"
          },
          "ntfy_token": {
            "type": "string"
          },
          "pushover_token": {
            "type": "string"
          },
          "pushover_user": {
            "type": "string"
          },
          "smtp_password": {
            "type": "string"
          },
          "ntfy_url": {
            "type": "string"
          },
          "webhook_url": {
            "type": "string"
          },
          "heartbeat_url": {
            "type": "string"
          },
          "smtp_user": {
            "type": "string"
          },
          "self_down": {
            "description": "The channel for Dokkit's own row when it stops taking reports or cannot reach its database. Empty is the loudest channel that is set up. One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity. One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "page",
                    "push",
                    "email",
                    "imessage",
                    "sms",
                    "digest",
                    "webhook",
                    "none"
                  ]
                }
              }
            ]
          },
          "self_degraded": {
            "description": "The channel for lesser trouble with Dokkit itself. Empty is push. One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity. One channel as text, or several as a list or as text with commas between. Each channel sends its own alert; `none` stands alone. A page and a push through PagerDuty together make one incident at the page's severity.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "page",
                    "push",
                    "email",
                    "imessage",
                    "sms",
                    "digest",
                    "webhook",
                    "none"
                  ]
                }
              }
            ]
          },
          "night_zone": {
            "type": "string",
            "description": "A time zone name such as America/Los_Angeles, in which the night and the digest time are kept. While it is empty the night is read in UTC plus night_offset."
          }
        }
      },
      "AlertSuggestion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The issue id. Use it to accept or dismiss."
          },
          "service": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "description": "How many times the issue opened in a month."
          }
        }
      },
      "Archive": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Such as logs-2025-01."
          },
          "size": {
            "type": "integer"
          },
          "back_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set while the month is loaded back."
          }
        }
      },
      "Release": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string"
          },
          "files": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "complete": {
            "type": "boolean",
            "description": "True when both `SHA256SUMS` and `SHA256SUMS.sig` are held."
          }
        }
      },
      "Certificate": {
        "type": "object",
        "properties": {
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The DNS names the certificate covers. A `*.` name covers one label."
          },
          "issuer": {
            "type": "string"
          },
          "not_after": {
            "type": "string",
            "format": "date-time",
            "description": "When the certificate runs out."
          },
          "self_signed": {
            "type": "boolean"
          },
          "sha256": {
            "type": "string",
            "description": "Hex SHA-256 fingerprint of the leaf certificate's DER."
          }
        }
      },
      "Certificates": {
        "type": "object",
        "properties": {
          "installed": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Certificate"
              },
              {
                "type": "null"
              }
            ],
            "description": "The certificate the server was installed with, which enrolled machines pin."
          },
          "uploaded": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Certificate"
              },
              {
                "type": "null"
              }
            ],
            "description": "The certificate put in place through the API, served to clients that ask for one of its names."
          },
          "renewal": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CertificateRenewal"
              },
              {
                "type": "null"
              }
            ],
            "description": "The outcome of the last attempt to get a certificate by itself, or null when there has been none."
          }
        }
      },
      "CertificateRenewal": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "ok": {
            "type": "boolean"
          },
          "note": {
            "type": "string",
            "description": "`Renewed, runs out 2027-01-19`, or the sentence that says why it failed."
          }
        }
      },
      "CertificateSettings": {
        "type": "object",
        "properties": {
          "acme_name": {
            "type": "string",
            "description": "The DNS name to get a certificate for, such as `example.dokkit.dev`. Empty when not set."
          },
          "acme_cloudflare_token": {
            "type": "string",
            "enum": [
              "set",
              "not set"
            ],
            "description": "A Cloudflare API token that may edit DNS in the name's zone. Never returned."
          },
          "acme_directory": {
            "type": "string",
            "description": "The certificate authority's ACME directory. Let's Encrypt production when not changed."
          }
        }
      },
      "CertificateSettingsBody": {
        "type": "object",
        "properties": {
          "acme_name": {
            "type": "string"
          },
          "acme_cloudflare_token": {
            "type": "string",
            "description": "An empty string clears it; leaving it out keeps it."
          },
          "acme_directory": {
            "type": "string",
            "description": "An https address. `https://acme-staging-v02.api.letsencrypt.org/directory` is Let's Encrypt's staging directory."
          }
        }
      }
    }
  }
}
