{
  "openapi": "3.1.0",
  "info": {
    "title": "ProofKit Operator API",
    "version": "1.1.0",
    "description": "Manage your ProofKit nodes from a script or an assistant: register a node, read its checks, change its settings, link an entry to an exit, and read your balance and payouts.\n\n- **Authentication**: every request carries an operator key, `Authorization: Bearer pko_<8 hex>_<48 hex>` (61 characters). A key is made by a signed-in operator session whose email is confirmed, with its second factor passed when the account has one: `POST /api/v1/operator/keys`, a route of the dashboard that is not part of this contract. It is shown once. A key has the scope `read`, or `read` and `manage`, and expires in 30 days, 90 days (the default), a year, or never. An account has at most 10 live keys. A key never makes, lists or revokes keys. The same routes also take a signed-in operator session's access token; an admin's is refused.\n- **Scopes**: `read` reads, `manage` also changes. Each operation says which it needs in `x-scope`: `read`, `manage`, or `none` for the two public routes.\n- **What no key can do**: link or change a wallet, approve or perform a withdrawal, request, prepare or send a claim, change the password, the email or the second factor, delete the account, make or revoke keys, read users' identities or support tickets, or reach anything of the admin. This API reads the money and moves none of it.\n- **Not yours means not found**: another operator's node, link or invitation answers exactly as one that does not exist: `404` with one body.\n- **Whose words**: an answer carries your own words and ours, and none that another account wrote. A node, a link or an invitation of yours shows the names, addresses and labels you gave it. An end of a link that is another operator's has our label for its name (`DE exit 05568fa5`), its address only if that is an IP address (`null` otherwise), and no key or SNI of its own.\n- **Money** is in nanoTON integers (1 TON = 1,000,000,000).\n- **Errors** use one envelope, `{\"error\": {\"code\", \"message\"}}`, with `details` for some codes: dispatch on `error.code`. Under `/api/v1/operator` an unknown path answers `404 NOT_FOUND` (\"Unknown operator API endpoint\") and a wrong method `405 METHOD_NOT_ALLOWED`. The one answer outside the envelope is a `GET /earnings` with a query that does not read; a body over 1 MB is refused by our proxy before it reaches the API.\n- **Rate limits**: 300 requests a minute per key, of which at most 60 writes a minute (every method but GET and HEAD), counted in fixed 60-second windows for both APPs together. `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` are on every answer to a counted key; a refused call is `429 RATE_LIMITED` with `Retry-After`. A flood guard in front allows 600 a minute per address.\n- **Idempotency**: `POST /nodes` takes an `Idempotency-Key`, kept 24 hours per account, so a retry registers nothing twice.\n- **A switch**: while the API is not open to your account every route answers as if it were not there. The two public routes answer once it is open to any account.\n- **Revisions**: this document is revision 1.1 (`info.version` is `1.1.0`). Every answer carries `X-Operator-Api-Version`. The API has one major version, `v1`, and it only grows: nothing is removed, renamed, retyped or given another meaning. A breaking change would be `/api/v2/operator`, announced at least 90 days ahead. Each change to the contract is a revision `1.N`, frozen in `versions/1.N.json` and recorded in the changelog. `x-since` on an operation is the revision it came in; on anything else it is there only for what came after 1.0.",
    "contact": {
      "name": "ProofKit",
      "url": "https://proofkit.org/#/support"
    }
  },
  "servers": [
    {
      "url": "https://proofkit.org/api/v1/operator",
      "description": "Production"
    }
  ],
  "security": [
    {
      "operatorKey": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Who the key belongs to, and what the account holds."
    },
    {
      "name": "Public",
      "description": "Where the network needs nodes and what a server needs. No key needed."
    },
    {
      "name": "Money",
      "description": "Earnings, the balance and past payouts. Read only."
    },
    {
      "name": "Nodes",
      "description": "Register, list, change, reinstall and delete nodes."
    },
    {
      "name": "Checks",
      "description": "A node's checks, its metrics and its events."
    },
    {
      "name": "Agent",
      "description": "Restart a node's agent, run its speed test."
    },
    {
      "name": "Links",
      "description": "Entry to exit links."
    },
    {
      "name": "Invitations",
      "description": "Invitations to private nodes."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "The calling account, its balance and the key that asks",
        "description": "Start here. It says who the key belongs to, what the account holds in nano TON, what registering one more node would lock (`balance.stake_nano`) and whether the balance pays it (`balance.covers_stake`), and which key made the call, never the key itself. `key` is `null` for a signed-in session. The same revision is in `revision` and in the `X-Operator-Api-Version` header.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "account": {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000100",
                    "email": "operator@example.com",
                    "email_verified": true,
                    "second_factor": true
                  },
                  "balance": {
                    "balance_nano": 2500000000,
                    "available_nano": 1500000000,
                    "locked_stake_nano": 1000000000,
                    "stake_nano": 1000000000,
                    "covers_stake": true
                  },
                  "key": {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000101",
                    "name": "agent",
                    "prefix": "pko_1a2b3c4d",
                    "scopes": [
                      "read",
                      "manage"
                    ],
                    "created_at": "2026-10-28T09:30:00Z",
                    "expires_at": "2027-01-26T09:30:00Z"
                  },
                  "revision": "1.0"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/wanted": {
      "get": {
        "operationId": "getWanted",
        "tags": [
          "Public"
        ],
        "summary": "Where the network needs nodes",
        "description": "A short list of places where a new node would help, and why. Public: it answers anyone with no key, and is there once the API is open to any account. Cacheable for five minutes.",
        "x-scope": "none",
        "x-since": "1.0",
        "security": [],
        "responses": {
          "200": {
            "description": "The places.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              },
              "Cache-Control": {
                "description": "`public, max-age=300`.",
                "schema": {
                  "type": "string"
                },
                "example": "public, max-age=300"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WantedRow"
                  }
                },
                "example": [
                  {
                    "type": "entry",
                    "country": "RU",
                    "where": "Russia",
                    "why": "Users in Russia connect through an entry inside the country. Entries at more providers keep them connected when one provider is blocked."
                  },
                  {
                    "type": "exit",
                    "country": "any",
                    "where": "A country that already has one exit",
                    "why": "A second exit keeps the country open to users while the first is down or full."
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/requirements": {
      "get": {
        "operationId": "getRequirements",
        "tags": [
          "Public"
        ],
        "summary": "What a server needs for a node, and the command that checks one",
        "description": "The least a server may have for an exit or an entry, what is recommended, the ports the node binds first, the architectures it runs on, the oldest kernel (`min_kernel`) and the kernel modules it needs, and `preflight`: a command to run on the server first. It reads the machine, changes nothing, and exits 0 when the minimum is met. Public, as `GET /wanted` is.",
        "x-scope": "none",
        "x-since": "1.0",
        "security": [],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": true,
            "description": "`exit` or `entry`. Anything else, or none, is `400 INVALID_TYPE`.",
            "schema": {
              "type": "string",
              "enum": [
                "exit",
                "entry"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requirements.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Requirements"
                },
                "example": {
                  "type": "exit",
                  "minimum": {
                    "cpu_cores": 1,
                    "ram_mb": 900,
                    "disk_free_mb": 1024
                  },
                  "recommended": {
                    "cpu_cores": 2,
                    "ram_mb": 2048
                  },
                  "ports": [
                    {
                      "proto": "tcp",
                      "port": 443
                    },
                    {
                      "proto": "udp",
                      "port": 443
                    }
                  ],
                  "architectures": {
                    "container": [
                      "x86_64"
                    ],
                    "systemd": [
                      "x86_64"
                    ]
                  },
                  "min_kernel": "5.6",
                  "kernel_modules": {
                    "container": [
                      "xt_addrtype"
                    ],
                    "systemd": []
                  },
                  "preflight": "curl -fsSL https://proofkit.org/install.sh | bash -s -- --check --type=exit"
                }
              }
            }
          },
          "400": {
            "description": "The type is not `exit` or `entry`.\n\nCodes: `INVALID_TYPE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_TYPE": {
                    "summary": "type is exit or entry",
                    "value": {
                      "error": {
                        "code": "INVALID_TYPE",
                        "message": "type is exit or entry"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/earnings": {
      "get": {
        "operationId": "getEarnings",
        "tags": [
          "Money"
        ],
        "summary": "Earnings by day and by role, and the nodes' reputation",
        "description": "What the account earned over the last 7 or 30 days: one entry for each UTC day with earnings, the split by the role the account's nodes had (alone, entry or exit), and the reputation of its nodes. Traffic of the account's own through its own nodes earns nothing and is not counted. A signed-in session reads it as the dashboard's Earnings tab always has.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "`7d` or `30d`. Any other value reads as `7d`.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d"
              ],
              "default": "7d"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The earnings.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Earnings"
                },
                "example": {
                  "buckets": [
                    {
                      "date": "2026-10-27",
                      "earned_nano": 120000000,
                      "sessions": 14
                    },
                    {
                      "date": "2026-10-28",
                      "earned_nano": 80000000,
                      "sessions": 9
                    }
                  ],
                  "by_route": {
                    "direct_nano": 0,
                    "entry_nano": 50000000,
                    "exit_nano": 150000000
                  },
                  "reputation": {
                    "proof_rate_pct": 100,
                    "uptime_pct": 99.5,
                    "probe_match_pct": 98,
                    "review_avg": 4.5,
                    "review_count": 12,
                    "capacity_pct": 8
                  }
                }
              }
            }
          },
          "400": {
            "description": "A query that does not read, such as `range` given twice. The framework refuses it before the handler, in plain text and not in the envelope.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Failed to deserialize query string"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/balance": {
      "get": {
        "operationId": "getBalance",
        "tags": [
          "Money"
        ],
        "summary": "The balance, what a claim can take, and whether a claim can be made",
        "description": "The four figures of the dashboard's balance, what is available once the stake is taken, and `claim`: whether a claim can be made now and, when it cannot, the first reason. This API reads the money and moves none of it: no key signs, requests or sends a claim or a withdrawal. When `claim.can_claim` is true, tell the operator to press Claim on the Wallet page; when it is false, `claim.reason` says why and what to tell them. `claimable_earnings_nano` and `claim` come from one reading of the account, so they agree.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The balance.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                },
                "example": {
                  "balance_nano": 2500000000,
                  "pending_earnings_nano": 400000000,
                  "claimable_earnings_nano": 400000000,
                  "locked_stake_nano": 1000000000,
                  "available_nano": 1500000000,
                  "claim": {
                    "can_claim": true,
                    "reason": null,
                    "min_payout_nano": 100000000
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/payouts": {
      "get": {
        "operationId": "listPayouts",
        "tags": [
          "Money"
        ],
        "summary": "Past payouts",
        "description": "The operator's newest 50 payouts on the escrow this deployment runs against, newest first: the rows the Wallet page lists. A payout never shows the claim that made it, its signature or the payers it names.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The payouts.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Payout"
                  }
                },
                "example": [
                  {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000051",
                    "amount_nano": 400000000,
                    "fee_nano": 0,
                    "status": "confirmed",
                    "to_address": "UQ…",
                    "created_at": "2026-10-27T10:00:00Z",
                    "confirmed_at": "2026-10-27T10:02:30Z",
                    "tx_hash": "c0ffee…"
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes": {
      "get": {
        "operationId": "listNodes",
        "tags": [
          "Nodes"
        ],
        "summary": "The caller's nodes",
        "description": "Every node of the caller's, a suspended one too (the dashboard's list leaves it out), each with what the dashboard's list gives, and: `state`, where the node is on its way to carrying users; `checks`, how many of its checks fail and warn; and `users_now`. It names no user. `state` is judged from the last pass of the checks, so it is at most 5 minutes old; `GET /nodes/{id}/checks` judges it now.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The nodes.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Node"
                  }
                },
                "example": [
                  {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                    "owner_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000100",
                    "service_id": null,
                    "node_type": "origin",
                    "name": "de-1",
                    "endpoint_host": "203.0.113.10",
                    "country": "DE",
                    "city": "Frankfurt",
                    "status": "active",
                    "last_heartbeat": "2026-10-28T09:30:00Z",
                    "total_bytes_relayed": 1073741824,
                    "xray_inbound_tag": null,
                    "xray_listen_port": 443,
                    "reality_public_key": null,
                    "reality_short_id": null,
                    "reality_sni": null,
                    "wg_public_key": null,
                    "wg_endpoint": null,
                    "ed25519_public_key": null,
                    "max_edges": null,
                    "foreign_edge_policy": "open",
                    "price_per_gb_nano": 5000000,
                    "description": null,
                    "speed_mbps": 1000,
                    "max_users": 200,
                    "active_sessions": 3,
                    "total_traffic": 1073741824,
                    "warp_enabled": true,
                    "torrent_guard": true,
                    "tg_max_ratio": 1.5,
                    "tg_max_session_gb": null,
                    "monthly_bandwidth_cap_gb": null,
                    "supported_tunnels": [
                      "wg"
                    ],
                    "preferred_tunnel": "wg",
                    "supported_transports": [
                      "vless_reality"
                    ],
                    "olcrtc_room": false,
                    "olcrtc_srv_up": null,
                    "olcrtc_rooms": [],
                    "olcrtc_carriers": [],
                    "olcrtc_carriers_available": [],
                    "olcrtc_carriers_gated": [],
                    "reach_checks": false,
                    "reach_checks_available": false,
                    "reach_checks_budget_bytes": 0,
                    "webtunnel": {
                      "available": false,
                      "enabled": false,
                      "domain": null,
                      "address": null,
                      "address6": null,
                      "records": {
                        "a": null,
                        "aaaa": null,
                        "caa": null
                      },
                      "checks": null,
                      "checked_at": null,
                      "links_ready": 0,
                      "links_total": 0
                    },
                    "hy2_cert_sha256": null,
                    "hy2_sni": null,
                    "hy2_port": null,
                    "stake_nano": 1000000000,
                    "is_public": true,
                    "auto_route": true,
                    "auto_routed": true,
                    "routing_paused_at": null,
                    "benchmark_down_mbps": null,
                    "benchmark_up_mbps": null,
                    "benchmark_latency_ms": null,
                    "benchmark_at": null,
                    "uptime_pct": 99.8,
                    "last_proof_status": null,
                    "last_proof_discrepancy": null,
                    "runtime": "container",
                    "agent_version": "0.4.27",
                    "agent_image": null,
                    "created_at": "2026-10-20T08:00:00Z",
                    "updated_at": "2026-10-28T09:30:00Z",
                    "metrics": null,
                    "sysinfo": null,
                    "last_probe": {
                      "at": "2026-10-28T09:30:00Z",
                      "ok": true
                    },
                    "state": "live",
                    "checks": {
                      "fail": 0,
                      "warn": 1
                    },
                    "users_now": 3
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/UnknownEndpoint"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "registerNode",
        "tags": [
          "Nodes"
        ],
        "summary": "Register a node",
        "description": "Registers a node for the account and locks its stake from the account's balance (`balance.stake_nano` of `GET /me` says how much; the stake is returned when the node is deleted). The answer holds the node's agent token and the commands that install the agent with it: `install_command` installs it in Docker, the way the dashboard shows it, and `install_command_systemd` installs it as a systemd service. Run one of them on the server as root, and nothing else needs the token. If the install stops, `install.sh --check` on the server says what the server lacks.\n\nWith an `Idempotency-Key` the registration runs once for an account and key. The first answer is kept for 24 hours; a retry with the same key and the same body is given it again, the agent token included, with `Idempotent-Replayed: true`, and registers nothing. The same key with another body is `422 IDEMPOTENCY_KEY_REUSED`. A request that finds the first one still running is `409 IDEMPOTENCY_IN_PROGRESS`: ask again in a few seconds. With the store that keeps the answers out of reach the answer is `503 IDEMPOTENCY_UNAVAILABLE` and nothing was done. Only a success is kept: a refusal frees the key at once. Without the header the registration runs as it is, and a retry could take a second stake.\n\nA node of yours at the same address that is offline or suspended, and of the same type, is registered again in place and keeps its stake and its earnings.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterNodeRequest"
              },
              "example": {
                "node_type": "origin",
                "endpoint_host": "203.0.113.10",
                "country": "DE",
                "name": "de-1",
                "price_per_gb_nano": 5000000
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The node is registered.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterNodeResponse"
                },
                "example": {
                  "node_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "agent_token": "AGENT-TOKEN-SHOWN-ONCE",
                  "install_command": "curl -fsSL https://proofkit.org/install.sh | bash -s -- --mode=docker --token=AGENT-TOKEN-SHOWN-ONCE --type=origin --country=DE",
                  "install_command_systemd": "curl -sSL https://proofkit.org/agent/binary -o dvpn-agent && chmod +x dvpn-agent && ./dvpn-agent --install --token=AGENT-TOKEN-SHOWN-ONCE --type=origin --coordinator-url=https://proofkit.org"
                }
              }
            }
          },
          "400": {
            "description": "The body is not what a registration takes, or a rule refuses it. Nothing was registered.\n\nCodes: `INVALID_BODY`, `INVALID_IDEMPOTENCY_KEY`, `INVALID_NODE_TYPE`, `INVALID_PRICE`, `PRICE_TOO_LOW`, `INVALID_NAME`, `INVALID_DESCRIPTION`, `INVALID_WG_SUBNET`, `INVALID_ENDPOINT`, `INVALID_COUNTRY`, `INVALID_SNI`, `INVALID_TUNNEL_TYPE`, `INVALID_TRANSPORT`, `INSUFFICIENT_BALANCE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_BODY": {
                    "summary": "Expected a JSON object with node_type, endpoint_host and country and, optionally, the node's other settings",
                    "value": {
                      "error": {
                        "code": "INVALID_BODY",
                        "message": "Expected a JSON object with node_type, endpoint_host and country and, optionally, the node's other settings"
                      }
                    }
                  },
                  "INVALID_IDEMPOTENCY_KEY": {
                    "summary": "The Idempotency-Key must be 1 to 255 visible ASCII characters, with no spaces",
                    "value": {
                      "error": {
                        "code": "INVALID_IDEMPOTENCY_KEY",
                        "message": "The Idempotency-Key must be 1 to 255 visible ASCII characters, with no spaces"
                      }
                    }
                  },
                  "INVALID_NODE_TYPE": {
                    "summary": "node_type must be 'edge' or 'origin'",
                    "value": {
                      "error": {
                        "code": "INVALID_NODE_TYPE",
                        "message": "node_type must be 'edge' or 'origin'"
                      }
                    }
                  },
                  "INVALID_PRICE": {
                    "summary": "price must be between 0.001 and 100 TON/GB",
                    "value": {
                      "error": {
                        "code": "INVALID_PRICE",
                        "message": "price must be between 0.001 and 100 TON/GB"
                      }
                    }
                  },
                  "PRICE_TOO_LOW": {
                    "summary": "an exit node's lowest price is 0.001 TON/GB; only an entry node can be free",
                    "value": {
                      "error": {
                        "code": "PRICE_TOO_LOW",
                        "message": "an exit node's lowest price is 0.001 TON/GB; only an entry node can be free"
                      }
                    }
                  },
                  "INVALID_NAME": {
                    "summary": "Node name must not contain < > or \"",
                    "value": {
                      "error": {
                        "code": "INVALID_NAME",
                        "message": "Node name must not contain < > or \""
                      }
                    }
                  },
                  "INVALID_DESCRIPTION": {
                    "summary": "Description must not contain < > or \"",
                    "value": {
                      "error": {
                        "code": "INVALID_DESCRIPTION",
                        "message": "Description must not contain < > or \""
                      }
                    }
                  },
                  "INVALID_WG_SUBNET": {
                    "summary": "wg_subnet must be a /24 inside 10.100.0.0/16, e.g. 10.100.13.0/24",
                    "value": {
                      "error": {
                        "code": "INVALID_WG_SUBNET",
                        "message": "wg_subnet must be a /24 inside 10.100.0.0/16, e.g. 10.100.13.0/24"
                      }
                    }
                  },
                  "INVALID_ENDPOINT": {
                    "summary": "endpoint_host is required",
                    "value": {
                      "error": {
                        "code": "INVALID_ENDPOINT",
                        "message": "endpoint_host is required"
                      }
                    }
                  },
                  "INVALID_COUNTRY": {
                    "summary": "country must be a 2-letter ISO code",
                    "value": {
                      "error": {
                        "code": "INVALID_COUNTRY",
                        "message": "country must be a 2-letter ISO code"
                      }
                    }
                  },
                  "INVALID_SNI": {
                    "summary": "reality_sni must be lowercase",
                    "value": {
                      "error": {
                        "code": "INVALID_SNI",
                        "message": "reality_sni must be lowercase"
                      }
                    }
                  },
                  "INVALID_TUNNEL_TYPE": {
                    "summary": "supported_tunnels must be one of 'wg', 'gre', 'gre_ipsec', 'amneziawg'",
                    "value": {
                      "error": {
                        "code": "INVALID_TUNNEL_TYPE",
                        "message": "supported_tunnels must be one of 'wg', 'gre', 'gre_ipsec', 'amneziawg'"
                      }
                    }
                  },
                  "INVALID_TRANSPORT": {
                    "summary": "supported_transports must contain only 'vless_reality', 'hysteria2', 'vless_xhttp', or 'olcrtc'",
                    "value": {
                      "error": {
                        "code": "INVALID_TRANSPORT",
                        "message": "supported_transports must contain only 'vless_reality', 'hysteria2', 'vless_xhttp', or 'olcrtc'"
                      }
                    }
                  },
                  "INSUFFICIENT_BALANCE": {
                    "summary": "Not enough balance to stake",
                    "value": {
                      "error": {
                        "code": "INSUFFICIENT_BALANCE",
                        "message": "Not enough balance to stake"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "A service the body names that does not exist, or the API is not open to your account.\n\nCodes: `NOT_FOUND`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "SERVICE_NOT_FOUND": {
                    "summary": "Service not found",
                    "value": {
                      "error": {
                        "code": "NOT_FOUND",
                        "message": "Service not found"
                      }
                    }
                  },
                  "API_NOT_OPEN": {
                    "summary": "The API is not open to this account",
                    "value": {
                      "error": {
                        "code": "NOT_FOUND",
                        "message": "Unknown operator API endpoint"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The address is taken, or the same `Idempotency-Key` is still running.\n\nCodes: `CONFLICT`, `WEBTUNNEL_ADDRESS_TAKEN`, `IDEMPOTENCY_IN_PROGRESS`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "CONFLICT_OTHER_OPERATOR": {
                    "summary": "Another operator has a node at that address",
                    "value": {
                      "error": {
                        "code": "CONFLICT",
                        "message": "This IP is already registered by another operator"
                      }
                    }
                  },
                  "CONFLICT_OWN_NODE": {
                    "summary": "You have an active, pending or degraded node there",
                    "value": {
                      "error": {
                        "code": "CONFLICT",
                        "message": "You already have a origin node registered on this IP"
                      }
                    }
                  },
                  "WEBTUNNEL_ADDRESS_TAKEN": {
                    "summary": "That address is another node's WebTunnel address: register the node at an address of its own",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_ADDRESS_TAKEN",
                        "message": "That address is another node's WebTunnel address: register the node at an address of its own"
                      }
                    }
                  },
                  "IDEMPOTENCY_IN_PROGRESS": {
                    "summary": "A request with this Idempotency-Key is still running. Ask again in a few seconds with the same key.",
                    "value": {
                      "error": {
                        "code": "IDEMPOTENCY_IN_PROGRESS",
                        "message": "A request with this Idempotency-Key is still running. Ask again in a few seconds with the same key."
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "description": "The `Idempotency-Key` was used for a request with another body.\n\nCodes: `IDEMPOTENCY_KEY_REUSED`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "IDEMPOTENCY_KEY_REUSED": {
                    "summary": "This Idempotency-Key was used for a request with another body. Use a new key for a new request.",
                    "value": {
                      "error": {
                        "code": "IDEMPOTENCY_KEY_REUSED",
                        "message": "This Idempotency-Key was used for a request with another body. Use a new key for a new request."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "The key cannot be honored right now, so nothing was done. Ask again in a moment with the same key.\n\nCodes: `IDEMPOTENCY_UNAVAILABLE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "IDEMPOTENCY_UNAVAILABLE": {
                    "summary": "The Idempotency-Key cannot be honored right now, so nothing was done. Ask again in a moment with the same key.",
                    "value": {
                      "error": {
                        "code": "IDEMPOTENCY_UNAVAILABLE",
                        "message": "The Idempotency-Key cannot be honored right now, so nothing was done. Ask again in a moment with the same key."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/nodes/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "get": {
        "operationId": "getNode",
        "tags": [
          "Nodes"
        ],
        "summary": "One node",
        "description": "One node of the caller's, as `GET /nodes` gives it.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The node.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Node"
                },
                "example": {
                  "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "owner_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000100",
                  "service_id": null,
                  "node_type": "origin",
                  "name": "de-1",
                  "endpoint_host": "203.0.113.10",
                  "country": "DE",
                  "city": "Frankfurt",
                  "status": "active",
                  "last_heartbeat": "2026-10-28T09:30:00Z",
                  "total_bytes_relayed": 1073741824,
                  "xray_inbound_tag": null,
                  "xray_listen_port": 443,
                  "reality_public_key": null,
                  "reality_short_id": null,
                  "reality_sni": null,
                  "wg_public_key": null,
                  "wg_endpoint": null,
                  "ed25519_public_key": null,
                  "max_edges": null,
                  "foreign_edge_policy": "open",
                  "price_per_gb_nano": 5000000,
                  "description": null,
                  "speed_mbps": 1000,
                  "max_users": 200,
                  "active_sessions": 3,
                  "total_traffic": 1073741824,
                  "warp_enabled": true,
                  "torrent_guard": true,
                  "tg_max_ratio": 1.5,
                  "tg_max_session_gb": null,
                  "monthly_bandwidth_cap_gb": null,
                  "supported_tunnels": [
                    "wg"
                  ],
                  "preferred_tunnel": "wg",
                  "supported_transports": [
                    "vless_reality"
                  ],
                  "olcrtc_room": false,
                  "olcrtc_srv_up": null,
                  "olcrtc_rooms": [],
                  "olcrtc_carriers": [],
                  "olcrtc_carriers_available": [],
                  "olcrtc_carriers_gated": [],
                  "reach_checks": false,
                  "reach_checks_available": false,
                  "reach_checks_budget_bytes": 0,
                  "webtunnel": {
                    "available": false,
                    "enabled": false,
                    "domain": null,
                    "address": null,
                    "address6": null,
                    "records": {
                      "a": null,
                      "aaaa": null,
                      "caa": null
                    },
                    "checks": null,
                    "checked_at": null,
                    "links_ready": 0,
                    "links_total": 0
                  },
                  "hy2_cert_sha256": null,
                  "hy2_sni": null,
                  "hy2_port": null,
                  "stake_nano": 1000000000,
                  "is_public": true,
                  "auto_route": true,
                  "auto_routed": true,
                  "routing_paused_at": null,
                  "benchmark_down_mbps": null,
                  "benchmark_up_mbps": null,
                  "benchmark_latency_ms": null,
                  "benchmark_at": null,
                  "uptime_pct": 99.8,
                  "last_proof_status": null,
                  "last_proof_discrepancy": null,
                  "runtime": "container",
                  "agent_version": "0.4.27",
                  "agent_image": null,
                  "created_at": "2026-10-20T08:00:00Z",
                  "updated_at": "2026-10-28T09:30:00Z",
                  "metrics": null,
                  "sysinfo": null,
                  "last_probe": {
                    "at": "2026-10-28T09:30:00Z",
                    "ok": true
                  },
                  "state": "live",
                  "checks": {
                    "fail": 0,
                    "warn": 1
                  },
                  "users_now": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateNode",
        "tags": [
          "Nodes"
        ],
        "summary": "Change a node's settings",
        "description": "Changes the settings the body names and leaves the rest. Every rule is asked before the first write, so a refused change writes nothing of its body and sends the agent nothing. A setting the node does not have is `400 UNKNOWN_FIELD`, with its name in `error.details.field`; a value of the wrong type or out of range is `400 INVALID_BODY` with `details.field` and `details.reason`. A price changes at most once in 24 hours and rises at most threefold at a time, unless the price now is 0; an exit's price stays within 0.001 to 100 TON. A change to an exit's `reality_sni` is pushed to the running agent and to the entries linked to it.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NodePatch"
              },
              "example": {
                "price_per_gb_nano": 6000000,
                "max_users": 300
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change was made.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateNodeAnswer"
                },
                "example": {
                  "status": "updated",
                  "node_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001"
                }
              }
            }
          },
          "400": {
            "description": "The body is not a JSON object of settings, or a rule refuses a value. Nothing was written.\n\nCodes: `UNKNOWN_FIELD`, `INVALID_BODY`, `INVALID_PRICE`, `PRICE_TOO_LOW`, `PRICE_COOLDOWN`, `PRICE_INCREASE_TOO_LARGE`, `INVALID_MAX_USERS`, `TOO_LONG`, `INVALID_DESCRIPTION`, `INVALID_NAME`, `INVALID_RATIO`, `INVALID_SESSION_GB`, `INVALID_CARRIERS`, `CARRIER_UNAVAILABLE`, `INVALID_REACH_CHECKS`, `INVALID_WEBTUNNEL`, `WEBTUNNEL_NOT_AN_ENTRY`, `WEBTUNNEL_NEEDS_CONTAINER`, `FIELD_NOT_EDITABLE`, `WEBTUNNEL_INCOMPLETE`, `INVALID_WEBTUNNEL_DOMAIN`, `WEBTUNNEL_DOMAIN_RESERVED`, `WEBTUNNEL_DOMAIN_PUBLIC_SUFFIX`, `INVALID_WEBTUNNEL_ADDRESS`, `INVALID_TUNNEL_TYPE`, `INVALID_TRANSPORT`, `INVALID_SNI`, `FOREIGN_LINK_REQUIRES_PRIVATE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "UNKNOWN_FIELD": {
                    "summary": "The change names a setting that nodes do not have",
                    "value": {
                      "error": {
                        "code": "UNKNOWN_FIELD",
                        "message": "The change names a setting that nodes do not have",
                        "details": {
                          "field": "color"
                        }
                      }
                    }
                  },
                  "INVALID_BODY": {
                    "summary": "A setting has a value of the wrong type or out of range",
                    "value": {
                      "error": {
                        "code": "INVALID_BODY",
                        "message": "A setting has a value of the wrong type or out of range",
                        "details": {
                          "field": "max_users",
                          "reason": "invalid type: string \"many\", expected i32"
                        }
                      }
                    }
                  },
                  "INVALID_PRICE": {
                    "summary": "price must be between 0.001 and 100 TON/GB",
                    "value": {
                      "error": {
                        "code": "INVALID_PRICE",
                        "message": "price must be between 0.001 and 100 TON/GB"
                      }
                    }
                  },
                  "PRICE_TOO_LOW": {
                    "summary": "an exit node's lowest price is 0.001 TON/GB; only an entry node can be free",
                    "value": {
                      "error": {
                        "code": "PRICE_TOO_LOW",
                        "message": "an exit node's lowest price is 0.001 TON/GB; only an entry node can be free"
                      }
                    }
                  },
                  "PRICE_COOLDOWN": {
                    "summary": "price can only be changed once per 24h (5h remaining)",
                    "value": {
                      "error": {
                        "code": "PRICE_COOLDOWN",
                        "message": "price can only be changed once per 24h (5h remaining)"
                      }
                    }
                  },
                  "PRICE_INCREASE_TOO_LARGE": {
                    "summary": "price increase limited to 3x (max 15000000 nano)",
                    "value": {
                      "error": {
                        "code": "PRICE_INCREASE_TOO_LARGE",
                        "message": "price increase limited to 3x (max 15000000 nano)"
                      }
                    }
                  },
                  "INVALID_MAX_USERS": {
                    "summary": "max_users must be between 0 and 10000",
                    "value": {
                      "error": {
                        "code": "INVALID_MAX_USERS",
                        "message": "max_users must be between 0 and 10000"
                      }
                    }
                  },
                  "TOO_LONG": {
                    "summary": "description max 500 chars",
                    "value": {
                      "error": {
                        "code": "TOO_LONG",
                        "message": "description max 500 chars"
                      }
                    }
                  },
                  "INVALID_DESCRIPTION": {
                    "summary": "Description must not contain < > or \"",
                    "value": {
                      "error": {
                        "code": "INVALID_DESCRIPTION",
                        "message": "Description must not contain < > or \""
                      }
                    }
                  },
                  "INVALID_NAME": {
                    "summary": "Node name must not contain < > or \"",
                    "value": {
                      "error": {
                        "code": "INVALID_NAME",
                        "message": "Node name must not contain < > or \""
                      }
                    }
                  },
                  "INVALID_RATIO": {
                    "summary": "tg_max_ratio must be between 0.1 and 10.0",
                    "value": {
                      "error": {
                        "code": "INVALID_RATIO",
                        "message": "tg_max_ratio must be between 0.1 and 10.0"
                      }
                    }
                  },
                  "INVALID_SESSION_GB": {
                    "summary": "tg_max_session_gb must be between 0.1 and 1000",
                    "value": {
                      "error": {
                        "code": "INVALID_SESSION_GB",
                        "message": "tg_max_session_gb must be between 0.1 and 1000"
                      }
                    }
                  },
                  "INVALID_CARRIERS": {
                    "summary": "olcrtc_carriers must list 'telemost', 'wbstream' or 'salutejazz', each at most once",
                    "value": {
                      "error": {
                        "code": "INVALID_CARRIERS",
                        "message": "olcrtc_carriers must list 'telemost', 'wbstream' or 'salutejazz', each at most once"
                      }
                    }
                  },
                  "CARRIER_UNAVAILABLE": {
                    "summary": "Telemost needs your own account: connect it under olcRTC carriers first",
                    "value": {
                      "error": {
                        "code": "CARRIER_UNAVAILABLE",
                        "message": "Telemost needs your own account: connect it under olcRTC carriers first",
                        "details": {
                          "carrier": "telemost"
                        }
                      }
                    }
                  },
                  "INVALID_REACH_CHECKS": {
                    "summary": "reach_checks must be true or false",
                    "value": {
                      "error": {
                        "code": "INVALID_REACH_CHECKS",
                        "message": "reach_checks must be true or false"
                      }
                    }
                  },
                  "INVALID_WEBTUNNEL": {
                    "summary": "webtunnel takes only enabled, domain, address and address6",
                    "value": {
                      "error": {
                        "code": "INVALID_WEBTUNNEL",
                        "message": "webtunnel takes only enabled, domain, address and address6"
                      }
                    }
                  },
                  "WEBTUNNEL_NOT_AN_ENTRY": {
                    "summary": "WebTunnel is a setting of an entry node, and this node is an exit",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_NOT_AN_ENTRY",
                        "message": "WebTunnel is a setting of an entry node, and this node is an exit"
                      }
                    }
                  },
                  "WEBTUNNEL_NEEDS_CONTAINER": {
                    "summary": "WebTunnel needs the node's agent to run in a container, and this node's does not",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_NEEDS_CONTAINER",
                        "message": "WebTunnel needs the node's agent to run in a container, and this node's does not"
                      }
                    }
                  },
                  "FIELD_NOT_EDITABLE": {
                    "summary": "WebTunnel is not open for this entry yet: ask ProofKit to open it",
                    "value": {
                      "error": {
                        "code": "FIELD_NOT_EDITABLE",
                        "message": "WebTunnel is not open for this entry yet: ask ProofKit to open it"
                      }
                    }
                  },
                  "WEBTUNNEL_INCOMPLETE": {
                    "summary": "Turning WebTunnel on needs a domain and an address",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_INCOMPLETE",
                        "message": "Turning WebTunnel on needs a domain and an address"
                      }
                    }
                  },
                  "INVALID_WEBTUNNEL_DOMAIN": {
                    "summary": "The domain must be a name such as ru1.example.com",
                    "value": {
                      "error": {
                        "code": "INVALID_WEBTUNNEL_DOMAIN",
                        "message": "The domain must be a name such as ru1.example.com"
                      }
                    }
                  },
                  "WEBTUNNEL_DOMAIN_RESERVED": {
                    "summary": "proofkit.org and every name under it are ProofKit's own: use a domain you control",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_DOMAIN_RESERVED",
                        "message": "proofkit.org and every name under it are ProofKit's own: use a domain you control"
                      }
                    }
                  },
                  "WEBTUNNEL_DOMAIN_PUBLIC_SUFFIX": {
                    "summary": "That name is a public suffix (like co.uk or github.io): use a domain registered to you",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_DOMAIN_PUBLIC_SUFFIX",
                        "message": "That name is a public suffix (like co.uk or github.io): use a domain registered to you"
                      }
                    }
                  },
                  "INVALID_WEBTUNNEL_ADDRESS": {
                    "summary": "address must be one public IPv4 address of the entry's host, written as an address alone",
                    "value": {
                      "error": {
                        "code": "INVALID_WEBTUNNEL_ADDRESS",
                        "message": "address must be one public IPv4 address of the entry's host, written as an address alone"
                      }
                    }
                  },
                  "INVALID_TUNNEL_TYPE": {
                    "summary": "supported_tunnels must be one of 'wg', 'gre', 'gre_ipsec', 'amneziawg'",
                    "value": {
                      "error": {
                        "code": "INVALID_TUNNEL_TYPE",
                        "message": "supported_tunnels must be one of 'wg', 'gre', 'gre_ipsec', 'amneziawg'"
                      }
                    }
                  },
                  "INVALID_TRANSPORT": {
                    "summary": "supported_transports must contain only 'vless_reality', 'hysteria2', 'vless_xhttp', or 'olcrtc'",
                    "value": {
                      "error": {
                        "code": "INVALID_TRANSPORT",
                        "message": "supported_transports must contain only 'vless_reality', 'hysteria2', 'vless_xhttp', or 'olcrtc'"
                      }
                    }
                  },
                  "INVALID_SNI": {
                    "summary": "reality_sni must be lowercase",
                    "value": {
                      "error": {
                        "code": "INVALID_SNI",
                        "message": "reality_sni must be lowercase"
                      }
                    }
                  },
                  "FOREIGN_LINK_REQUIRES_PRIVATE": {
                    "summary": "This entry node is linked to another operator's exit node that admits entries by invitation, and such a node stays private.",
                    "value": {
                      "error": {
                        "code": "FOREIGN_LINK_REQUIRES_PRIVATE",
                        "message": "This entry node is linked to another operator's exit node that admits entries by invitation, and such a node stays private."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "409": {
            "description": "The change clashes with another node's.\n\nCodes: `CONFLICT`, `WEBTUNNEL_DOMAIN_TAKEN`, `WEBTUNNEL_ADDRESS_TAKEN`, `WEBTUNNEL_MAIN_ADDRESS_UNKNOWN`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "CONFLICT": {
                    "summary": "An exit's reality_sni clashes with another exit's on a shared entry",
                    "value": {
                      "error": {
                        "code": "CONFLICT",
                        "message": "SNI 'www.example.com' collides on 1 edge(s): edge in DE (collides with origin …). Unlink this origin from those edges first, or pick a different SNI."
                      }
                    }
                  },
                  "WEBTUNNEL_DOMAIN_TAKEN": {
                    "summary": "Another node uses that domain for WebTunnel, or one above or below it",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_DOMAIN_TAKEN",
                        "message": "Another node uses that domain for WebTunnel, or one above or below it"
                      }
                    }
                  },
                  "WEBTUNNEL_ADDRESS_TAKEN": {
                    "summary": "That address is the entry's own main address or another node's: WebTunnel needs an address of its own",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_ADDRESS_TAKEN",
                        "message": "That address is the entry's own main address or another node's: WebTunnel needs an address of its own"
                      }
                    }
                  },
                  "WEBTUNNEL_MAIN_ADDRESS_UNKNOWN": {
                    "summary": "This entry is registered by a name that gave no addresses in time. Nothing was saved: try again in a minute",
                    "value": {
                      "error": {
                        "code": "WEBTUNNEL_MAIN_ADDRESS_UNKNOWN",
                        "message": "This entry is registered by a name that gave no addresses in time. Nothing was saved: try again in a minute"
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteNode",
        "tags": [
          "Nodes"
        ],
        "summary": "Delete a node",
        "description": "Deletes the node with everything that depends on it, closes its sessions and refunds its stake. A node whose earnings can be claimed (at least the claim minimum) is not deleted: the answer is `409 UNSETTLED_EARNINGS`, with `details.unsettled_nano` and `details.claim_minimum_nano`, which is the `claim.min_payout_nano` of `GET /balance`. Claim on the Wallet page first. A refused delete leaves the agent connected.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The node is deleted.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteNodeAnswer"
                },
                "example": {
                  "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "deleted": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "409": {
            "description": "The node has earnings that can be claimed.\n\nCodes: `UNSETTLED_EARNINGS`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "UNSETTLED_EARNINGS": {
                    "summary": "Cannot remove node: 400000000 nano-TON of unsettled earnings would wait on a node that no longer exists. Claim these earnings first via the operator dashboard, then retry.",
                    "value": {
                      "error": {
                        "code": "UNSETTLED_EARNINGS",
                        "message": "Cannot remove node: 400000000 nano-TON of unsettled earnings would wait on a node that no longer exists. Claim these earnings first via the operator dashboard, then retry.",
                        "details": {
                          "unsettled_nano": 400000000,
                          "claim_minimum_nano": 100000000
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/checks": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "get": {
        "operationId": "getNodeChecks",
        "tags": [
          "Checks"
        ],
        "summary": "A node's checks, judged now",
        "description": "The node's state and each check behind it, with its words and its fix, judged at the read from what the platform records and from outside connections over every transport the node offers. A check's `id`, `subject` and `status` are final. A suspended node can be read: its checks say why. Each check's `since` is when it took the status it has.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The state and the checks.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeChecks"
                },
                "example": {
                  "node_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "state": "live",
                  "checks": [
                    {
                      "id": "disk_low",
                      "subject": "",
                      "status": "warn",
                      "blocks": null,
                      "since": "2026-10-28T08:50:00Z",
                      "title": "Disk space",
                      "detail": "0.8 GB free on the agent's disk.",
                      "fix": {
                        "text": "Free some space or grow the disk. Below 1 GB, pulling a new agent image can fail and leave the node offline. The commands below remove images that no container uses and no tag names, such as the agent's old image.",
                        "commands": [
                          {
                            "runtime": "docker",
                            "run": "docker image prune -f"
                          },
                          {
                            "runtime": "podman",
                            "run": "podman image prune -f"
                          }
                        ],
                        "help": "/#/help#operator.checks"
                      }
                    },
                    {
                      "id": "transport_unreachable",
                      "subject": "reality",
                      "status": "ok",
                      "blocks": null,
                      "since": "2026-10-27T10:00:00Z",
                      "title": "Reality on TCP 443",
                      "detail": "A test connection from outside got through.",
                      "fix": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/checks/run": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "post": {
        "operationId": "runNodeChecks",
        "tags": [
          "Checks"
        ],
        "summary": "Ask for a new round of outside test connections",
        "description": "Asks for a new round of outside test connections to the node's lines. An exit asks for its own lines; an entry asks each exit it has an active link to for the lines through itself. The round is dialled within a minute or so: read the checks again. Once in 5 minutes for a node: a second ask in the window is `429 RATE_LIMITED` with `Retry-After`, the seconds that are left of the window. Where nothing can be asked the answer says why: `NODE_NOT_CONNECTED` (the node is pending, offline or degraded: its lines are checked once it connects), `NODE_SUSPENDED`, or `NO_LINES` (an entry with no active link to an exit). It needs only `read`, because it changes no setting.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "202": {
            "description": "The round was asked for.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChecksRun"
                },
                "example": {
                  "asked": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "409": {
            "description": "Nothing can be asked now.\n\nCodes: `NODE_NOT_CONNECTED`, `NODE_SUSPENDED`, `NO_LINES`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "NODE_NOT_CONNECTED": {
                    "summary": "The node's agent is not connected, so no test can run now. Its lines are checked once it connects.",
                    "value": {
                      "error": {
                        "code": "NODE_NOT_CONNECTED",
                        "message": "The node's agent is not connected, so no test can run now. Its lines are checked once it connects."
                      }
                    }
                  },
                  "NODE_SUSPENDED": {
                    "summary": "The node is suspended, so no test can run. Its checks say why and how to bring it back.",
                    "value": {
                      "error": {
                        "code": "NODE_SUSPENDED",
                        "message": "The node is suspended, so no test can run. Its checks say why and how to bring it back."
                      }
                    }
                  },
                  "NO_LINES": {
                    "summary": "This entry has no active link to an exit yet, so there is no line to test.",
                    "value": {
                      "error": {
                        "code": "NO_LINES",
                        "message": "This entry has no active link to an exit yet, so there is no line to test."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/metrics": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "get": {
        "operationId": "getNodeMetrics",
        "tags": [
          "Checks"
        ],
        "summary": "A node's metrics over a range",
        "description": "The points the dashboard's charts draw, with what they are drawn against: capacity, the connection table's size, the cores and the overload levels. An hour by the minute when no range is given; longer ranges are by 5 minutes or by the hour.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "How far back to read the metrics: `1h`, `6h`, `24h`, `7d`, `30d` or `90d`. The default is `1h`. Any other value is `400 INVALID_RANGE`.",
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "6h",
                "24h",
                "7d",
                "30d",
                "90d"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The metrics.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetricsHistory"
                },
                "example": {
                  "range": "1h",
                  "resolution_secs": 60,
                  "capacity_mbps": 1000,
                  "conntrack_max": 262144,
                  "nproc": 2,
                  "thresholds": {
                    "mem": 90,
                    "load_per_cpu": 2,
                    "conntrack_pct": 80,
                    "listen_overflows": 1
                  },
                  "points": [
                    {
                      "ts": 1793179800,
                      "users": 3,
                      "down_mbps": 12.5,
                      "up_mbps": 3.1,
                      "cpu": 0.4,
                      "mem": 41.2,
                      "load": 0.5,
                      "connections": 120,
                      "disk_free_mb": 20480,
                      "overloaded": null,
                      "uplink_rx_mbps": null,
                      "uplink_tx_mbps": null,
                      "ping_ms": null,
                      "ping_jitter_ms": null,
                      "latency_ms": null,
                      "jitter_ms": null,
                      "conntrack": null,
                      "conntrack_full_drops": null,
                      "listen_overflows": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The range is not one of those the history keeps.\n\nCodes: `INVALID_RANGE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_RANGE": {
                    "summary": "range is one of 1h, 6h, 24h, 7d, 30d, 90d",
                    "value": {
                      "error": {
                        "code": "INVALID_RANGE",
                        "message": "range is one of 1h, 6h, 24h, 7d, 30d, 90d"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/events": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "get": {
        "operationId": "getNodeEvents",
        "tags": [
          "Checks"
        ],
        "summary": "A node's events over a range",
        "description": "The node's events, newest first: agent starts and version changes, overload windows, links going up and down, and olcRTC carrier events. A day when no range is given.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "How far back to read the events: `1h`, `6h`, `24h`, `7d`, `30d` or `90d`. The default is `24h`. Any other value is `400 INVALID_RANGE`.",
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "6h",
                "24h",
                "7d",
                "30d",
                "90d"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The events.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeEvent"
                  }
                },
                "example": [
                  {
                    "at": "2026-10-28T09:30:00Z",
                    "kind": "overload_start",
                    "detail": {
                      "reason": "memory"
                    }
                  }
                ]
              }
            }
          },
          "400": {
            "description": "The range is not one of those the history keeps.\n\nCodes: `INVALID_RANGE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_RANGE": {
                    "summary": "range is one of 1h, 6h, 24h, 7d, 30d, 90d",
                    "value": {
                      "error": {
                        "code": "INVALID_RANGE",
                        "message": "range is one of 1h, 6h, 24h, 7d, 30d, 90d"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/reinstall": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "post": {
        "operationId": "reinstallNode",
        "tags": [
          "Agent"
        ],
        "summary": "A new agent token and install command",
        "description": "Makes a new agent token for the node and the commands that install the agent with it (`install_command` in Docker, `install_command_systemd` as a systemd service), with the node's own options. For a node whose `runtime` is `systemd`, which `GET /nodes/{id}` shows, use `install_command_systemd`. The running agent keeps working on the old token until the new install connects. A node that is not `active` is set to `pending` until the new agent connects. The token is in the answer this once.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The new token and command.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterNodeResponse"
                },
                "example": {
                  "node_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "agent_token": "AGENT-TOKEN-SHOWN-ONCE",
                  "install_command": "curl -fsSL https://proofkit.org/install.sh | bash -s -- --mode=docker --token=AGENT-TOKEN-SHOWN-ONCE --type=origin --country=DE",
                  "install_command_systemd": "curl -sSL https://proofkit.org/agent/binary -o dvpn-agent && chmod +x dvpn-agent && ./dvpn-agent --install --token=AGENT-TOKEN-SHOWN-ONCE --type=origin --coordinator-url=https://proofkit.org"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/restart": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "post": {
        "operationId": "restartNode",
        "tags": [
          "Agent"
        ],
        "summary": "Restart the node's agent",
        "description": "Tells the node's agent to restart. It restarts the whole agent, so XRay and WARP restart with it and every connection through the node drops: it is the cure for a sick but connected agent. Only an `active` or a `degraded` node: `suspended` is `409 NODE_SUSPENDED`, and any other status, or an agent that is not on the control channel, is `409 NODE_NOT_CONNECTED`. A node's agent is restarted 3 times an hour, counted for the node: past that the answer is `429 RATE_LIMITED` with `Retry-After`. A restart whose command could not be sent (`409 NODE_NOT_CONNECTED`) is not counted. `sent: true` means the command went out, not that the agent has restarted yet. The audit log records the restart and whether a key or a session asked.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "202": {
            "description": "The command was sent.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Restart"
                },
                "example": {
                  "sent": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "409": {
            "description": "The agent cannot be restarted from here.\n\nCodes: `NODE_SUSPENDED`, `NODE_NOT_CONNECTED`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "NODE_SUSPENDED": {
                    "summary": "The node is suspended, so its agent is not restarted. Its checks say why and how to bring it back.",
                    "value": {
                      "error": {
                        "code": "NODE_SUSPENDED",
                        "message": "The node is suspended, so its agent is not restarted. Its checks say why and how to bring it back."
                      }
                    }
                  },
                  "NODE_NOT_CONNECTED": {
                    "summary": "The node's agent is not connected, so it cannot be restarted from here. Check that the agent is running on the server.",
                    "value": {
                      "error": {
                        "code": "NODE_NOT_CONNECTED",
                        "message": "The node's agent is not connected, so it cannot be restarted from here. Check that the agent is running on the server."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/benchmark": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "post": {
        "operationId": "benchmarkNode",
        "tags": [
          "Agent"
        ],
        "summary": "Run a speed test of the node",
        "description": "Runs the speed test the dashboard runs and answers with its reading, which is kept on the node. The owner's alone. A node is tested once a minute, the dashboard's tests counted with these: a second in the window is `429 RATE_LIMITED` with `Retry-After`, the seconds that are left of the minute. The call waits for the agent for up to 45 seconds. An agent that is away is `400 NODE_OFFLINE`, one that cannot fetch the test file whole is `400 BENCHMARK_FAILED`, and one that does not answer is `400 BENCHMARK_TIMEOUT`.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The reading.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Benchmark"
                },
                "example": {
                  "node_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "down_mbps": 940.5,
                  "up_mbps": 410.2,
                  "latency_ms": 12.3,
                  "benchmark_at": "2026-10-28T09:30:00Z"
                }
              }
            }
          },
          "400": {
            "description": "The speed test did not run.\n\nCodes: `BENCHMARK_FAILED`, `NODE_OFFLINE`, `BENCHMARK_TIMEOUT`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "BENCHMARK_FAILED": {
                    "summary": "The agent could not fetch the test file whole",
                    "value": {
                      "error": {
                        "code": "BENCHMARK_FAILED",
                        "message": "The agent could not fetch the test file whole"
                      }
                    }
                  },
                  "NODE_OFFLINE": {
                    "summary": "Agent not connected",
                    "value": {
                      "error": {
                        "code": "NODE_OFFLINE",
                        "message": "Agent not connected"
                      }
                    }
                  },
                  "BENCHMARK_TIMEOUT": {
                    "summary": "Agent did not reply within 45s",
                    "value": {
                      "error": {
                        "code": "BENCHMARK_TIMEOUT",
                        "message": "Agent did not reply within 45s"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/links": {
      "get": {
        "operationId": "listLinks",
        "tags": [
          "Links"
        ],
        "summary": "Links that end at the caller's nodes",
        "description": "The links that have an end at a node of the caller's, as the dashboard lists them, with both ends' names and each link's health. A node that is another operator's is not described by what its owner wrote: its end is named by our own label, `DE exit 05568fa5` or `RU entry 05568fa5` (see `edge_name` and `origin_name`), and its address is given only when it is an IP address and is `null` otherwise (see `edge_endpoint_host` and `origin_endpoint_host`). The caller's own nodes keep their names and addresses. `node` narrows the list to one node's links, which must be the caller's own.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "parameters": [
          {
            "name": "node",
            "in": "query",
            "required": false,
            "description": "One of the caller's nodes. Another operator's, a node nobody has and an id that is no node id are `404 NOT_FOUND` \"Node not found\".",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The links.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Link"
                  }
                },
                "example": [
                  {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000031",
                    "edge_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                    "origin_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000002",
                    "wg_edge_ip": "10.100.7.2",
                    "wg_origin_ip": "10.100.7.1",
                    "priority": 1,
                    "status": "active",
                    "tunnel_type": "wg",
                    "created_at": "2026-10-21T08:00:00Z",
                    "updated_at": "2026-10-28T09:30:00Z",
                    "edge_name": "ru-1",
                    "edge_country": "RU",
                    "edge_endpoint_host": "198.51.100.7",
                    "origin_name": "de-1",
                    "origin_country": "DE",
                    "origin_endpoint_host": "203.0.113.10",
                    "edge_supported_tunnels": [
                      "wg"
                    ],
                    "origin_supported_tunnels": [
                      "wg"
                    ],
                    "active_sessions": 3,
                    "edge_is_mine": true,
                    "origin_is_mine": true,
                    "health": {
                      "down_mbps": 8.2,
                      "up_mbps": 1.1,
                      "last_bucket_at": "2026-10-28T09:30:00Z",
                      "loss_down_pct": 0,
                      "loss_up_pct": null,
                      "spark": [
                        0.5,
                        1.2,
                        0.9
                      ]
                    }
                  },
                  {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000032",
                    "edge_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                    "origin_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000003",
                    "wg_edge_ip": "10.100.8.2",
                    "wg_origin_ip": "10.100.8.1",
                    "priority": 1,
                    "status": "active",
                    "tunnel_type": "wg",
                    "created_at": "2026-10-21T08:00:00Z",
                    "updated_at": "2026-10-28T09:30:00Z",
                    "edge_name": "ru-1",
                    "edge_country": "RU",
                    "edge_endpoint_host": "198.51.100.7",
                    "origin_name": "DE exit 0e5a1c2b",
                    "origin_country": "DE",
                    "origin_endpoint_host": "203.0.113.77",
                    "edge_supported_tunnels": [
                      "wg"
                    ],
                    "origin_supported_tunnels": [
                      "wg"
                    ],
                    "active_sessions": 0,
                    "edge_is_mine": true,
                    "origin_is_mine": false,
                    "health": null
                  },
                  {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000033",
                    "edge_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000004",
                    "origin_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000002",
                    "wg_edge_ip": "10.100.7.3",
                    "wg_origin_ip": "10.100.7.1",
                    "priority": 1,
                    "status": "active",
                    "tunnel_type": "wg",
                    "created_at": "2026-10-22T11:00:00Z",
                    "updated_at": "2026-10-28T09:30:00Z",
                    "edge_name": "RU entry 0e5a1c2b",
                    "edge_country": "RU",
                    "edge_endpoint_host": null,
                    "origin_name": "de-1",
                    "origin_country": "DE",
                    "origin_endpoint_host": "203.0.113.10",
                    "edge_supported_tunnels": [
                      "wg"
                    ],
                    "origin_supported_tunnels": [
                      "wg"
                    ],
                    "active_sessions": 1,
                    "edge_is_mine": false,
                    "origin_is_mine": true,
                    "health": null
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createLink",
        "tags": [
          "Links"
        ],
        "summary": "Link an entry to an exit",
        "description": "Links the caller's entry to an exit, as the dashboard does: the exit's policy decides (an exit of another operator may be closed to foreign entries or admit those it whitelists), the tunnel is negotiated or taken from `tunnel_type`, and both agents are told. The entry must be the caller's: another operator's, or none, is `404 NOT_FOUND` \"Node not found\". The exit may be another operator's. The answer is `200`, the dashboard's, except that it leaves out the WireGuard key of an exit that is another operator's (`wg_origin_pubkey` is `null`): the key is its owner's text, and the link is made with it all the same.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateLinkRequest"
              },
              "example": {
                "edge_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                "origin_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000002"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkResponse"
                },
                "example": {
                  "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000031",
                  "edge_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001",
                  "origin_id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000002",
                  "wg_edge_pubkey": null,
                  "wg_origin_pubkey": null,
                  "wg_edge_ip": "10.100.7.2",
                  "wg_origin_ip": "10.100.7.1",
                  "priority": 1,
                  "status": "pending",
                  "tunnel_type": "wg",
                  "created_at": "2026-10-28T09:30:00Z",
                  "updated_at": "2026-10-28T09:30:00Z"
                }
              }
            }
          },
          "400": {
            "description": "The two nodes cannot be linked, or the body is not what a link takes.\n\nCodes: `INVALID_BODY`, `INVALID_NODE_TYPE`, `NODE_SUSPENDED`, `EDGE_MUST_BE_PRIVATE`, `MISSING_SNI`, `INVALID_TUNNEL_TYPE`, `TUNNEL_NOT_SUPPORTED`, `MISSING_SUBNET`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_BODY": {
                    "summary": "Expected a JSON object with edge_id and origin_id and, optionally, tunnel_type",
                    "value": {
                      "error": {
                        "code": "INVALID_BODY",
                        "message": "Expected a JSON object with edge_id and origin_id and, optionally, tunnel_type"
                      }
                    }
                  },
                  "INVALID_NODE_TYPE": {
                    "summary": "origin_id must reference an origin node",
                    "value": {
                      "error": {
                        "code": "INVALID_NODE_TYPE",
                        "message": "origin_id must reference an origin node"
                      }
                    }
                  },
                  "NODE_SUSPENDED": {
                    "summary": "Origin node is deleted/suspended",
                    "value": {
                      "error": {
                        "code": "NODE_SUSPENDED",
                        "message": "Origin node is deleted/suspended"
                      }
                    }
                  },
                  "EDGE_MUST_BE_PRIVATE": {
                    "summary": "This exit node admits entry nodes by invitation, and only private ones: make the entry node private, then link it.",
                    "value": {
                      "error": {
                        "code": "EDGE_MUST_BE_PRIVATE",
                        "message": "This exit node admits entry nodes by invitation, and only private ones: make the entry node private, then link it."
                      }
                    }
                  },
                  "MISSING_SNI": {
                    "summary": "Origin node has no reality_sni configured. The agent must connect first to register its SNI.",
                    "value": {
                      "error": {
                        "code": "MISSING_SNI",
                        "message": "Origin node has no reality_sni configured. The agent must connect first to register its SNI."
                      }
                    }
                  },
                  "INVALID_TUNNEL_TYPE": {
                    "summary": "Must be 'wg', 'gre', 'gre_ipsec', or 'amneziawg'",
                    "value": {
                      "error": {
                        "code": "INVALID_TUNNEL_TYPE",
                        "message": "Must be 'wg', 'gre', 'gre_ipsec', or 'amneziawg'"
                      }
                    }
                  },
                  "TUNNEL_NOT_SUPPORTED": {
                    "summary": "edge node(s) do not advertise gre_ipsec in supported_tunnels (edge=[\"wg\"], origin=[\"wg\", \"gre_ipsec\"]). Reinstall those node(s) with --tunnel=gre_ipsec first, then retry the switch.",
                    "value": {
                      "error": {
                        "code": "TUNNEL_NOT_SUPPORTED",
                        "message": "edge node(s) do not advertise gre_ipsec in supported_tunnels (edge=[\"wg\"], origin=[\"wg\", \"gre_ipsec\"]). Reinstall those node(s) with --tunnel=gre_ipsec first, then retry the switch."
                      }
                    }
                  },
                  "MISSING_SUBNET": {
                    "summary": "Origin has no WG subnet configured",
                    "value": {
                      "error": {
                        "code": "MISSING_SUBNET",
                        "message": "Origin has no WG subnet configured"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The exit's policy refuses the entry, or the call is not allowed.\n\nCodes: `FORBIDDEN`, `INSUFFICIENT_SCOPE`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "EXIT_POLICY": {
                    "summary": "Origin node does not accept foreign edges",
                    "value": {
                      "error": {
                        "code": "FORBIDDEN",
                        "message": "Origin node does not accept foreign edges"
                      }
                    }
                  },
                  "INSUFFICIENT_SCOPE": {
                    "summary": "This key cannot manage nodes. Make a key with the manage scope.",
                    "value": {
                      "error": {
                        "code": "INSUFFICIENT_SCOPE",
                        "message": "This key cannot manage nodes. Make a key with the manage scope."
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The entry is not yours or not a node, the exit does not exist, or the API is not open to your account.\n\nCodes: `NOT_FOUND`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "ENTRY_NOT_FOUND": {
                    "summary": "Node not found",
                    "value": {
                      "error": {
                        "code": "NOT_FOUND",
                        "message": "Node not found"
                      }
                    }
                  },
                  "EXIT_NOT_FOUND": {
                    "summary": "Origin node not found",
                    "value": {
                      "error": {
                        "code": "NOT_FOUND",
                        "message": "Origin node not found"
                      }
                    }
                  },
                  "API_NOT_OPEN": {
                    "summary": "The API is not open to this account",
                    "value": {
                      "error": {
                        "code": "NOT_FOUND",
                        "message": "Unknown operator API endpoint"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The link clashes with another: the pair is linked already, or two exits on the entry would share an SNI. The message of an SNI clash names the SNI only when the exit is yours. For another operator's exit the SNI is its owner's words, so the message names the origin it clashes with by its id and says that it is the SNI of the exit you are linking.\n\nCodes: `CONFLICT`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "CONFLICT": {
                    "summary": "SNI collision: origin … already uses SNI … on this edge. Each origin on an edge must have a unique SNI.",
                    "value": {
                      "error": {
                        "code": "CONFLICT",
                        "message": "SNI collision: origin … already uses SNI … on this edge. Each origin on an edge must have a unique SNI."
                      }
                    }
                  },
                  "CONFLICT_OTHER_OPERATORS_EXIT": {
                    "summary": "SNI collision: origin … already uses the SNI of the origin you are linking on this edge. Each origin on an edge must have a unique SNI.",
                    "value": {
                      "error": {
                        "code": "CONFLICT",
                        "message": "SNI collision: origin … already uses the SNI of the origin you are linking on this edge. Each origin on an edge must have a unique SNI."
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/links/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/LinkId"
        }
      ],
      "delete": {
        "operationId": "deleteLink",
        "tags": [
          "Links"
        ],
        "summary": "Break a link",
        "description": "Breaks a link that has an end at a node of the caller's, either end. The sessions on it close and both agents take the tunnel down.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The link is broken.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteLinkAnswer"
                },
                "example": {
                  "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000031",
                  "status": "broken",
                  "closed_sessions": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/LinkNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/links/{id}/tunnel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/LinkId"
        }
      ],
      "post": {
        "operationId": "switchLinkTunnel",
        "tags": [
          "Links"
        ],
        "summary": "Switch a link to another tunnel type",
        "description": "Switches a link that has an end at a node of the caller's to another tunnel type. Both nodes must support it. The old tunnel is taken down, the sessions on the link close and the new tunnel is set up. The link is looked at before the body. A link that already uses the type answers `switched: false` and does nothing.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SwitchTunnelRequest"
              },
              "example": {
                "tunnel_type": "gre_ipsec"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tunnel was switched, or already was the one asked for.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SwitchTunnelAnswer"
                },
                "example": {
                  "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000031",
                  "tunnel_type": "gre_ipsec",
                  "switched": true,
                  "closed_sessions": 3
                }
              }
            }
          },
          "400": {
            "description": "The body is not what a switch takes, or a node cannot carry the type.\n\nCodes: `INVALID_BODY`, `INVALID_TUNNEL_TYPE`, `TUNNEL_NOT_SUPPORTED`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_BODY": {
                    "summary": "Expected a JSON object with tunnel_type",
                    "value": {
                      "error": {
                        "code": "INVALID_BODY",
                        "message": "Expected a JSON object with tunnel_type"
                      }
                    }
                  },
                  "INVALID_TUNNEL_TYPE": {
                    "summary": "Must be 'wg', 'gre', 'gre_ipsec', or 'amneziawg'",
                    "value": {
                      "error": {
                        "code": "INVALID_TUNNEL_TYPE",
                        "message": "Must be 'wg', 'gre', 'gre_ipsec', or 'amneziawg'"
                      }
                    }
                  },
                  "TUNNEL_NOT_SUPPORTED": {
                    "summary": "edge node(s) do not advertise gre_ipsec in supported_tunnels (edge=[\"wg\"], origin=[\"wg\", \"gre_ipsec\"]). Reinstall those node(s) with --tunnel=gre_ipsec first, then retry the switch.",
                    "value": {
                      "error": {
                        "code": "TUNNEL_NOT_SUPPORTED",
                        "message": "edge node(s) do not advertise gre_ipsec in supported_tunnels (edge=[\"wg\"], origin=[\"wg\", \"gre_ipsec\"]). Reinstall those node(s) with --tunnel=gre_ipsec first, then retry the switch."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/LinkNotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/invitations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        }
      ],
      "get": {
        "operationId": "listInvitations",
        "tags": [
          "Invitations"
        ],
        "summary": "A node's invitations",
        "description": "The node's invitations, newest first, with the grants each has handed out. A token is never in the list: only its first characters.",
        "x-scope": "read",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The invitations.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Invitation"
                  }
                },
                "example": [
                  {
                    "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000041",
                    "token_prefix": "abcd1234",
                    "label": "friends",
                    "max_uses": 10,
                    "used_count": 2,
                    "expires_at": "2026-11-27T09:30:00Z",
                    "revoked_at": null,
                    "created_at": "2026-10-28T09:30:00Z",
                    "active_grants": 2
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createInvitation",
        "tags": [
          "Invitations"
        ],
        "summary": "Invite someone to a private node",
        "description": "Makes an invitation to one of the caller's private nodes. The token and the link that opens it are in the answer this once. A body with nothing in it, or none, makes an invitation with no label, no limit and no expiry. An operator makes 30 invitations a minute: past that the answer is `429 RATE_LIMITED` with `Retry-After`, the seconds that are left of the minute.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvitationRequest"
              },
              "example": {
                "label": "friends",
                "max_uses": 10
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The invitation.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateInvitationResponse"
                },
                "example": {
                  "id": "0e5a1c2b-7d3f-4a9e-8b6c-000000000041",
                  "token": "abcd1234efgh5678",
                  "magic_link": "https://proofkit.org/#/invite/abcd1234efgh5678",
                  "token_prefix": "abcd1234",
                  "label": "friends",
                  "max_uses": 10,
                  "used_count": 0,
                  "expires_at": null,
                  "created_at": "2026-10-28T09:30:00Z"
                }
              }
            }
          },
          "400": {
            "description": "The limits are not good, or the node is public.\n\nCodes: `INVALID_BODY`, `INVALID_MAX_USES`, `INVALID_EXPIRY`, `LABEL_TOO_LONG`, `NODE_IS_PUBLIC`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_BODY": {
                    "summary": "Expected a JSON object with, optionally, label, max_uses and expires_at",
                    "value": {
                      "error": {
                        "code": "INVALID_BODY",
                        "message": "Expected a JSON object with, optionally, label, max_uses and expires_at"
                      }
                    }
                  },
                  "INVALID_MAX_USES": {
                    "summary": "max_uses must be > 0",
                    "value": {
                      "error": {
                        "code": "INVALID_MAX_USES",
                        "message": "max_uses must be > 0"
                      }
                    }
                  },
                  "INVALID_EXPIRY": {
                    "summary": "expires_at is in the past",
                    "value": {
                      "error": {
                        "code": "INVALID_EXPIRY",
                        "message": "expires_at is in the past"
                      }
                    }
                  },
                  "LABEL_TOO_LONG": {
                    "summary": "label max 100 chars",
                    "value": {
                      "error": {
                        "code": "LABEL_TOO_LONG",
                        "message": "label max 100 chars"
                      }
                    }
                  },
                  "NODE_IS_PUBLIC": {
                    "summary": "Cannot create invitation for a public node - flip to private first",
                    "value": {
                      "error": {
                        "code": "NODE_IS_PUBLIC",
                        "message": "Cannot create invitation for a public node - flip to private first"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NodeNotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/nodes/{id}/invitations/{iid}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/NodeId"
        },
        {
          "$ref": "#/components/parameters/InvitationId"
        }
      ],
      "delete": {
        "operationId": "revokeInvitation",
        "tags": [
          "Invitations"
        ],
        "summary": "Revoke an invitation",
        "description": "Revokes an invitation of the caller's node: its link stops working. With `revoke_grants=true` the grants it handed out go with it. An invitation that is already revoked, with no grants to revoke, is `400 ALREADY_REVOKED`.",
        "x-scope": "manage",
        "x-since": "1.0",
        "security": [
          {
            "operatorKey": []
          }
        ],
        "parameters": [
          {
            "name": "revoke_grants",
            "in": "query",
            "required": false,
            "description": "`true` to revoke the grants the invitation handed out as well. Default `false`. Any other value is `400 INVALID_QUERY`.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The invitation is revoked.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokeInvitationAnswer"
                },
                "example": {
                  "ok": true,
                  "grants_revoked": 0
                }
              }
            }
          },
          "400": {
            "description": "Nothing to revoke, or the query is not what the call takes.\n\nCodes: `ALREADY_REVOKED`, `INVALID_QUERY`.",
            "headers": {
              "X-Operator-Api-Version": {
                "$ref": "#/components/headers/X-Operator-Api-Version"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/X-Request-ID"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "ALREADY_REVOKED": {
                    "summary": "Invitation is already revoked and no active grants remain",
                    "value": {
                      "error": {
                        "code": "ALREADY_REVOKED",
                        "message": "Invitation is already revoked and no active grants remain"
                      }
                    }
                  },
                  "INVALID_QUERY": {
                    "summary": "revoke_grants is true or false",
                    "value": {
                      "error": {
                        "code": "INVALID_QUERY",
                        "message": "revoke_grants is true or false"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/InvitationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "operatorKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pko_…",
        "description": "An operator key: `pko_<8 hex>_<48 hex>`, 61 characters, sent as `Authorization: Bearer`. Made by a signed-in operator session and shown once; it works only on this contract. A partner key (`pk_`) or a vantage key (`pkv_`) is not one."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The one error envelope. Dispatch on `error.code`; the message is for people.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable code, such as `INVALID_OPERATOR_KEY`."
              },
              "message": {
                "type": "string",
                "description": "Human-readable message."
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Facts about the refusal, for the codes that carry them: `UNSETTLED_EARNINGS` (`unsettled_nano`, `claim_minimum_nano`), `UNKNOWN_FIELD` (`field`), `INVALID_BODY` of a change (`field`, `reason`) and `CARRIER_UNAVAILABLE` (`carrier`). Absent otherwise."
              }
            }
          }
        }
      },
      "Me": {
        "type": "object",
        "description": "The account that asks, what it holds, the key that asks and the revision of the contract.",
        "required": [
          "account",
          "balance",
          "key",
          "revision"
        ],
        "properties": {
          "account": {
            "$ref": "#/components/schemas/MeAccount",
            "description": "The account that asks."
          },
          "balance": {
            "$ref": "#/components/schemas/MeBalance",
            "description": "What the account holds, and what a new node would lock from it."
          },
          "key": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MeKey"
              },
              {
                "type": "null"
              }
            ],
            "description": "The key that asks, as the key list shows it and never the key itself; `null` when a signed-in session asks."
          },
          "revision": {
            "type": "string",
            "pattern": "^1\\.\\d+$",
            "description": "The revision of the contract this answer follows, as `1.N`: the same as the `X-Operator-Api-Version` header."
          }
        }
      },
      "MeAccount": {
        "type": "object",
        "description": "The calling account.",
        "required": [
          "id",
          "email",
          "email_verified",
          "second_factor"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The account's id."
          },
          "email": {
            "type": "string",
            "description": "The account's own address, the only email address this API says."
          },
          "email_verified": {
            "type": "boolean",
            "description": "Whether the address is confirmed. A key is made only for an account whose address is."
          },
          "second_factor": {
            "type": "boolean",
            "description": "Whether an authenticator or Telegram two-factor is on for the account."
          }
        }
      },
      "MeBalance": {
        "type": "object",
        "description": "What the account holds, in nano TON.",
        "required": [
          "balance_nano",
          "available_nano",
          "locked_stake_nano",
          "stake_nano",
          "covers_stake"
        ],
        "properties": {
          "balance_nano": {
            "type": "integer",
            "format": "int64",
            "description": "What the account may spend: deposits and unpaid earnings, less charges and withdrawals. The stake its nodes lock is not taken from it here."
          },
          "available_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The balance less the stake its nodes lock: what a new node's stake is paid from. It is negative when the stake is more than the balance."
          },
          "locked_stake_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The stake the account's nodes lock. It comes back when a node is deleted."
          },
          "stake_nano": {
            "type": "integer",
            "format": "int64",
            "description": "What registering one more node locks; `0` when staking is off."
          },
          "covers_stake": {
            "type": "boolean",
            "description": "Whether `available_nano` pays that stake. A registration without enough is `INSUFFICIENT_BALANCE`."
          }
        }
      },
      "MeKey": {
        "type": "object",
        "description": "A key, as the key list shows it.",
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "created_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The key's id."
          },
          "name": {
            "type": "string",
            "description": "The name given to the key when it was made."
          },
          "prefix": {
            "type": "string",
            "pattern": "^pko_[0-9a-f]{8}$",
            "description": "The first 12 characters of the key: `pko_` and 8 hex characters. The rest is never shown again."
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KeyScope"
            },
            "description": "What the key may do. Every key has `read`; `manage` comes with it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the key was made."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the key stops working; `null` for a key that never does."
          }
        }
      },
      "WantedRow": {
        "type": "object",
        "description": "A place where the network needs a node.",
        "required": [
          "type",
          "country",
          "where",
          "why"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "entry",
              "exit"
            ],
            "description": "The kind of node wanted: `entry` or `exit`."
          },
          "country": {
            "type": "string",
            "description": "A two-letter country code, or `any`."
          },
          "where": {
            "type": "string",
            "description": "The place, in words."
          },
          "why": {
            "type": "string",
            "description": "Why the network needs a node there."
          }
        }
      },
      "Requirements": {
        "type": "object",
        "description": "What a server needs to run a node of one type, and the command that checks one.",
        "required": [
          "type",
          "minimum",
          "recommended",
          "ports",
          "architectures",
          "min_kernel",
          "kernel_modules",
          "preflight"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "exit",
              "entry"
            ],
            "description": "The node type asked for."
          },
          "minimum": {
            "$ref": "#/components/schemas/RequirementsMinimum",
            "description": "The least a server may have."
          },
          "recommended": {
            "$ref": "#/components/schemas/RequirementsRecommended",
            "description": "What the network suggests."
          },
          "ports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RequiredPort"
            },
            "description": "The ports the node binds first. The preflight asks whether they are free."
          },
          "architectures": {
            "$ref": "#/components/schemas/Architectures",
            "description": "The CPU architectures a node runs on."
          },
          "min_kernel": {
            "type": "string",
            "description": "The oldest Linux kernel the Docker install runs on, as `major.minor`: the agent's WireGuard is the kernel's own, which has been in Linux since 5.6. The preflight fails a Docker install on an older kernel, with the check `kernel_version`, and the installer stops with the same words.",
            "x-since": "1.1"
          },
          "kernel_modules": {
            "$ref": "#/components/schemas/KernelModules",
            "description": "The kernel modules a way of installing needs that a server may lack.",
            "x-since": "1.1"
          },
          "preflight": {
            "type": "string",
            "description": "The command that runs the installer's preflight: it reads the machine, changes nothing, and exits 0 when the minimum is met and 1 when it is not. For the Docker install, the default, it also checks that the kernel is `min_kernel` or newer and has the modules in `kernel_modules`, and says how to fix what is missing; `--mode=systemd` leaves both out."
          }
        }
      },
      "RequirementsMinimum": {
        "type": "object",
        "description": "The least a server may have for a node.",
        "required": [
          "cpu_cores",
          "ram_mb",
          "disk_free_mb"
        ],
        "properties": {
          "cpu_cores": {
            "type": "integer",
            "format": "int64",
            "description": "CPU cores."
          },
          "ram_mb": {
            "type": "integer",
            "format": "int64",
            "description": "Memory in MiB. It is 900 and not 1024 because a 1 GB server reports 950 to 990 MiB."
          },
          "disk_free_mb": {
            "type": "integer",
            "format": "int64",
            "description": "Free disk space in MiB."
          }
        }
      },
      "RequirementsRecommended": {
        "type": "object",
        "description": "What the network suggests for a node.",
        "required": [
          "cpu_cores",
          "ram_mb"
        ],
        "properties": {
          "cpu_cores": {
            "type": "integer",
            "format": "int64",
            "description": "CPU cores."
          },
          "ram_mb": {
            "type": "integer",
            "format": "int64",
            "description": "Memory in MiB."
          }
        }
      },
      "RequiredPort": {
        "type": "object",
        "description": "A port the node binds.",
        "required": [
          "proto",
          "port"
        ],
        "properties": {
          "proto": {
            "type": "string",
            "enum": [
              "tcp",
              "udp"
            ],
            "description": "The protocol."
          },
          "port": {
            "type": "integer",
            "format": "int32",
            "minimum": 1,
            "maximum": 65535,
            "description": "The port number."
          }
        }
      },
      "Architectures": {
        "type": "object",
        "description": "The architectures a node runs on, as `uname -m` spells them.",
        "required": [
          "container",
          "systemd"
        ],
        "properties": {
          "container": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For the Docker install, which runs the agent image."
          },
          "systemd": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For the install that runs the agent binary as a systemd service."
          }
        }
      },
      "KernelModules": {
        "type": "object",
        "description": "The kernel modules each way of installing needs, by the name `modinfo` knows them.",
        "required": [
          "container",
          "systemd"
        ],
        "properties": {
          "container": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For the Docker install. Docker's network rules use `xt_addrtype`, which AlmaLinux, CentOS Stream and RHEL 10 ship in `kernel-modules-extra`: the preflight fails a server whose running kernel has none, and says how to install it.",
            "x-since": "1.1"
          },
          "systemd": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For the install that runs the agent binary as a systemd service: none.",
            "x-since": "1.1"
          }
        }
      },
      "Earnings": {
        "type": "object",
        "description": "Daily earnings, how they split by role and the reputation of the operator's nodes.",
        "required": [
          "buckets",
          "by_route",
          "reputation"
        ],
        "properties": {
          "buckets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EarningsBucket"
            },
            "description": "One entry for each UTC day with earnings in the window, oldest first."
          },
          "by_route": {
            "$ref": "#/components/schemas/RouteSplit",
            "description": "The window's earnings split by the role the operator's node had in the route."
          },
          "reputation": {
            "$ref": "#/components/schemas/Reputation",
            "description": "The operator's reputation across its nodes."
          }
        }
      },
      "EarningsBucket": {
        "type": "object",
        "description": "One UTC day of earnings.",
        "required": [
          "date",
          "earned_nano",
          "sessions"
        ],
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "The UTC day, `YYYY-MM-DD`."
          },
          "earned_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Earned that day, in nano TON."
          },
          "sessions": {
            "type": "integer",
            "format": "int64",
            "description": "How many sessions earned that day."
          }
        }
      },
      "RouteSplit": {
        "type": "object",
        "description": "Earnings by the role of the node, in nano TON.",
        "required": [
          "direct_nano",
          "entry_nano",
          "exit_nano"
        ],
        "properties": {
          "direct_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Earned as the only node of a one-hop route."
          },
          "entry_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Earned as the entry of a two-hop route."
          },
          "exit_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Earned as the exit of a two-hop route."
          }
        }
      },
      "Reputation": {
        "type": "object",
        "description": "Reputation across the operator's nodes. A figure that cannot be computed is `null`, never made up.",
        "required": [
          "proof_rate_pct",
          "uptime_pct",
          "probe_match_pct",
          "review_avg",
          "review_count",
          "capacity_pct"
        ],
        "properties": {
          "proof_rate_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Share of the last 30 days' checkpoints that were verified, in percent."
          },
          "uptime_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Average uptime of the nodes, in percent."
          },
          "probe_match_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Share of the last 30 days' bandwidth-assessable probes that matched, in percent."
          },
          "review_avg": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Average rating from users' reviews."
          },
          "review_count": {
            "type": "integer",
            "format": "int64",
            "description": "How many reviews the nodes have."
          },
          "capacity_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Active and pending sessions against the capacity the nodes declare, in percent."
          }
        }
      },
      "Balance": {
        "type": "object",
        "description": "The balance, what is pending and claimable, and whether a claim can be made now.",
        "required": [
          "balance_nano",
          "pending_earnings_nano",
          "claimable_earnings_nano",
          "locked_stake_nano",
          "available_nano",
          "claim"
        ],
        "properties": {
          "balance_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Spendable: deposits and unpaid earnings, less charges and withdrawals. The stake is not taken from it."
          },
          "pending_earnings_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Earnings not paid yet: unsettled, or held by a pending claim."
          },
          "claimable_earnings_nano": {
            "type": "integer",
            "format": "int64",
            "description": "Of the pending earnings, what a claim can take now."
          },
          "locked_stake_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The stake the account's nodes lock."
          },
          "available_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The balance less the locked stake."
          },
          "claim": {
            "$ref": "#/components/schemas/Claim",
            "description": "Whether a claim can be made now. No key makes one: the operator presses Claim on the Wallet page."
          }
        }
      },
      "Claim": {
        "type": "object",
        "description": "Whether a claim can be made now.",
        "required": [
          "can_claim",
          "reason",
          "min_payout_nano"
        ],
        "properties": {
          "can_claim": {
            "type": "boolean",
            "description": "Whether the Wallet page can make a claim now."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "WALLET_NOT_LINKED",
              "CLAIM_NEEDS_REVIEW",
              "CLAIM_SETTLING",
              "BELOW_MINIMUM",
              null
            ],
            "description": "Why not, when it cannot: `WALLET_NOT_LINKED` (no wallet is linked: the operator links one on the Wallet page), `CLAIM_NEEDS_REVIEW` (an earlier claim was paid after it was voided and needs a manual check: the operator opens a ticket on the Support page), `CLAIM_SETTLING` (the Wallet page would refuse a claim now without asking the chain, because an earlier claim's window closed less than 5 minutes ago or a payout is being booked for it: ask again in a few minutes) or `BELOW_MINIMUM` (less than `min_payout_nano` can be claimed); `null` when it can. An earlier claim that was never sent and whose window has run out is none of these: pressing Claim replaces it, so tell the operator to press Claim. `null` is read without asking the chain: if the chain then says an earlier claim was paid and is not booked yet, or cannot be read, the press itself is refused with `CLAIM_SETTLING` and says so."
          },
          "min_payout_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The smallest claim, in nano TON."
          }
        }
      },
      "Payout": {
        "type": "object",
        "description": "A payout: what the operator was paid, or is to be paid, and when. Never the claim it was made by.",
        "required": [
          "id",
          "amount_nano",
          "fee_nano",
          "status",
          "to_address",
          "created_at",
          "confirmed_at",
          "tx_hash"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The payout's id."
          },
          "amount_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The payout's amount, in nano TON."
          },
          "fee_nano": {
            "type": "integer",
            "format": "int64",
            "description": "A fee recorded on the payout, in nano TON; `0` when none is."
          },
          "status": {
            "type": "string",
            "description": "Where the payout is: `pending` (a claim was signed and is not seen on-chain yet), `processing`, `confirmed`, `failed`, `cancelled` or `voided`."
          },
          "to_address": {
            "type": [
              "string",
              "null"
            ],
            "description": "The wallet the payout goes to."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the payout was made."
          },
          "confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was confirmed on-chain; `null` until then."
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "The payout's transaction on-chain, once it is booked."
          }
        }
      },
      "Node": {
        "type": "object",
        "description": "A node of the caller's: everything the dashboard's node list says, and where the node is on its way to carrying users, how many of its checks fail and warn, and how many users it carries now. It names no user.",
        "required": [
          "id",
          "owner_id",
          "service_id",
          "node_type",
          "name",
          "endpoint_host",
          "country",
          "city",
          "status",
          "last_heartbeat",
          "total_bytes_relayed",
          "xray_inbound_tag",
          "xray_listen_port",
          "reality_public_key",
          "reality_short_id",
          "reality_sni",
          "wg_public_key",
          "wg_endpoint",
          "ed25519_public_key",
          "max_edges",
          "foreign_edge_policy",
          "price_per_gb_nano",
          "description",
          "speed_mbps",
          "max_users",
          "active_sessions",
          "total_traffic",
          "warp_enabled",
          "torrent_guard",
          "tg_max_ratio",
          "tg_max_session_gb",
          "monthly_bandwidth_cap_gb",
          "supported_tunnels",
          "preferred_tunnel",
          "supported_transports",
          "olcrtc_room",
          "olcrtc_srv_up",
          "olcrtc_rooms",
          "olcrtc_carriers",
          "olcrtc_carriers_available",
          "olcrtc_carriers_gated",
          "reach_checks",
          "reach_checks_available",
          "reach_checks_budget_bytes",
          "webtunnel",
          "hy2_cert_sha256",
          "hy2_sni",
          "hy2_port",
          "stake_nano",
          "is_public",
          "auto_route",
          "auto_routed",
          "routing_paused_at",
          "benchmark_down_mbps",
          "benchmark_up_mbps",
          "benchmark_latency_ms",
          "benchmark_at",
          "uptime_pct",
          "last_proof_status",
          "last_proof_discrepancy",
          "runtime",
          "agent_version",
          "agent_image",
          "created_at",
          "updated_at",
          "metrics",
          "sysinfo",
          "last_probe",
          "state",
          "checks",
          "users_now"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The node's id."
          },
          "owner_id": {
            "type": "string",
            "format": "uuid",
            "description": "The id of the account that owns the node: the caller's."
          },
          "service_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The service the node was registered under; `null` for almost every node."
          },
          "node_type": {
            "type": "string",
            "enum": [
              "origin",
              "edge"
            ],
            "description": "`origin` for an exit, `edge` for an entry."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name the operator gave the node."
          },
          "endpoint_host": {
            "type": "string",
            "description": "The address the node is reached at."
          },
          "country": {
            "type": "string",
            "description": "The node's country, a two-letter code."
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "The node's city."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "degraded",
              "suspended",
              "offline"
            ],
            "description": "The node's status: `pending` (waiting for its agent), `active`, `degraded`, `offline` or `suspended`. `state` says more."
          },
          "last_heartbeat": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the agent last reported; `null` if it never has."
          },
          "total_bytes_relayed": {
            "type": "integer",
            "format": "int64",
            "description": "Bytes the node has relayed."
          },
          "xray_inbound_tag": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tag of the node's XRay inbound, once its agent reported one."
          },
          "xray_listen_port": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The port the node's VLESS inbound listens on."
          },
          "reality_public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Reality public key the agent reported."
          },
          "reality_short_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Reality short id the agent reported."
          },
          "reality_sni": {
            "type": [
              "string",
              "null"
            ],
            "description": "The cover site's name that an exit shows to anyone who is not a user."
          },
          "wg_public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The node's WireGuard public key."
          },
          "wg_endpoint": {
            "type": [
              "string",
              "null"
            ],
            "description": "The node's WireGuard endpoint."
          },
          "ed25519_public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key the node signs its checkpoints with."
          },
          "max_edges": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "A limit on the entries linked to an exit, as the node row holds it; `null` when none is set."
          },
          "foreign_edge_policy": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "whitelist",
              "closed",
              null
            ],
            "description": "Whether other operators' entries may link to this exit: `open`, `whitelist` or `closed`."
          },
          "price_per_gb_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The price per GB in nano TON, before the platform's fee."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "The node's description."
          },
          "speed_mbps": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The speed the operator declares, in Mbit/s."
          },
          "max_users": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The most users the node takes at once."
          },
          "active_sessions": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Sessions through the node now."
          },
          "total_traffic": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Bytes carried by every session through the node."
          },
          "warp_enabled": {
            "type": "boolean",
            "description": "Whether an exit sends its users' TCP through Cloudflare WARP."
          },
          "torrent_guard": {
            "type": "boolean",
            "description": "Whether an exit ends sessions that look like torrenting."
          },
          "tg_max_ratio": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Torrent Guard: the upload to download ratio past which a session ends."
          },
          "tg_max_session_gb": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Torrent Guard: the session size in GB past which a session ends; `null` for no limit."
          },
          "monthly_bandwidth_cap_gb": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "A monthly cap in GB; `null` for none."
          },
          "supported_tunnels": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "wg",
                "gre",
                "gre_ipsec",
                "amneziawg"
              ]
            },
            "description": "The tunnel types the node supports."
          },
          "preferred_tunnel": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg",
              null
            ],
            "description": "The tunnel type tried first."
          },
          "supported_transports": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "vless_reality",
                "hysteria2",
                "vless_xhttp",
                "olcrtc"
              ]
            },
            "description": "The transports an exit offers users."
          },
          "olcrtc_room": {
            "type": "boolean",
            "description": "Whether the exit has an olcRTC room on any carrier."
          },
          "olcrtc_srv_up": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "The agent's last report of whether its olcRTC server is up; `null` when it has reported none."
          },
          "olcrtc_rooms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OlcrtcRoom"
            },
            "description": "The olcRTC rooms the exit runs now, in the network's carrier order, then by room number. Empty while none is provisioned."
          },
          "olcrtc_carriers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "telemost",
                "wbstream",
                "mtslink",
                "salutejazz",
                "vkcalls"
              ]
            },
            "description": "The olcRTC carriers the operator chose for the node."
          },
          "olcrtc_carriers_available": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "telemost",
                "wbstream",
                "mtslink",
                "salutejazz",
                "vkcalls"
              ]
            },
            "description": "The carriers usable on the node now."
          },
          "olcrtc_carriers_gated": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "telemost",
                "wbstream",
                "mtslink",
                "salutejazz",
                "vkcalls"
              ]
            },
            "description": "The carriers the platform has opened for the node, whether or not the account that needs connecting is."
          },
          "reach_checks": {
            "type": "boolean",
            "description": "Whether the node's agent runs the reachability checks while the platform lists the node."
          },
          "reach_checks_available": {
            "type": "boolean",
            "description": "Whether the platform offers that setting for the node."
          },
          "reach_checks_budget_bytes": {
            "type": "integer",
            "format": "int64",
            "description": "What the checks may use a day, in bytes."
          },
          "webtunnel": {
            "$ref": "#/components/schemas/WebtunnelBox",
            "description": "The entry's WebTunnel box. Every node has one; a node that is not an entry reads `available: false`."
          },
          "hy2_cert_sha256": {
            "type": [
              "string",
              "null"
            ],
            "description": "The SHA-256 of the exit's Hysteria2 certificate."
          },
          "hy2_sni": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's Hysteria2 cover name."
          },
          "hy2_port": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The exit's Hysteria2 port."
          },
          "stake_nano": {
            "type": "integer",
            "format": "int64",
            "description": "The stake the node locks, in nano TON. It comes back when the node is deleted."
          },
          "is_public": {
            "type": "boolean",
            "description": "`false` for a private node, which only an invitation reaches."
          },
          "auto_route": {
            "type": "boolean",
            "description": "Whether the platform routes users to the node by itself: an admin approves it. A user who names the node connects either way."
          },
          "auto_routed": {
            "type": "boolean",
            "description": "Whether that rule takes the node right now: approved, and priced at or under the ceiling."
          },
          "routing_paused_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Since when routing to the node is paused; `null` when it is not."
          },
          "benchmark_down_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The last speed test's download, in Mbit/s."
          },
          "benchmark_up_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The last speed test's upload, in Mbit/s."
          },
          "benchmark_latency_ms": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The last speed test's latency, in milliseconds."
          },
          "benchmark_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the last speed test ran."
          },
          "uptime_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Uptime counted from heartbeats, in percent."
          },
          "last_proof_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "The verification status of the latest checkpoint."
          },
          "last_proof_discrepancy": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The latest checkpoint's discrepancy."
          },
          "runtime": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "systemd",
              "container",
              null
            ],
            "description": "How the agent runs: `systemd` or `container`."
          },
          "agent_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "The agent's version."
          },
          "agent_image": {
            "type": [
              "string",
              "null"
            ],
            "description": "The agent's image, for a container."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the node was registered."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the node last changed."
          },
          "metrics": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "The metrics the agent reported last, as the dashboard draws them; `null` when none is cached."
          },
          "sysinfo": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "The system information the agent reported, as the dashboard shows it; `null` when none is cached."
          },
          "last_probe": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LastProbe"
              },
              {
                "type": "null"
              }
            ],
            "description": "The newest finished probe through the node in the last day; `null` when there is none."
          },
          "state": {
            "$ref": "#/components/schemas/NodeState",
            "description": "Where the node is on its way to carrying users. It is judged from the node's row and what the last pass of its checks recorded, so it is at most one pass (5 minutes) old; `GET /nodes/{id}/checks` judges it now."
          },
          "checks": {
            "$ref": "#/components/schemas/NodeCheckCounts",
            "description": "How many of the node's checks the last pass recorded failing, and warning."
          },
          "users_now": {
            "type": "integer",
            "format": "int64",
            "description": "The sessions the node carries now."
          }
        }
      },
      "NodeCheckCounts": {
        "type": "object",
        "description": "How many checks fail and warn.",
        "required": [
          "fail",
          "warn"
        ],
        "properties": {
          "fail": {
            "type": "integer",
            "format": "int64",
            "description": "Checks recorded as `fail`."
          },
          "warn": {
            "type": "integer",
            "format": "int64",
            "description": "Checks recorded as `warn`."
          }
        }
      },
      "OlcrtcRoom": {
        "type": "object",
        "description": "One olcRTC room of an exit.",
        "required": [
          "provider",
          "room_no",
          "srv_up",
          "retiring"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "telemost",
              "wbstream",
              "mtslink",
              "salutejazz",
              "vkcalls"
            ],
            "description": "The carrier."
          },
          "room_no": {
            "type": "integer",
            "format": "int32",
            "description": "Which of the carrier's rooms: 1, or 2 for a second Telemost room."
          },
          "srv_up": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the agent's server for the room last reported up; `null` until it has reported at all, which is not the same as down."
          },
          "retiring": {
            "type": "boolean",
            "description": "Whether the room is being given up: off every line, its server kept through a 24 hour drain."
          }
        }
      },
      "LastProbe": {
        "type": "object",
        "description": "When a probe last checked an exit, and whether it went through.",
        "required": [
          "at",
          "ok"
        ],
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When the probe ran."
          },
          "ok": {
            "type": "boolean",
            "description": "Whether the download through the node went through."
          },
          "reason": {
            "type": "string",
            "description": "Why it did not: the runner's code, or `content_mismatch` for wrong test-file content. Absent when it did."
          }
        }
      },
      "WebtunnelBox": {
        "type": "object",
        "description": "An entry's WebTunnel box: whether it is offered, what is filled in, the DNS records to create, what the checks said and how many links are ready.",
        "required": [
          "available",
          "enabled",
          "domain",
          "address",
          "address6",
          "records",
          "checks",
          "checked_at",
          "links_ready",
          "links_total"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "Whether the node is offered the box: an entry, in a container, listed by the platform, on an agent that carries WebTunnel."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the box is on."
          },
          "domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "The operator's domain for the entry's names."
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's extra IPv4 address."
          },
          "address6": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's extra IPv6 address."
          },
          "records": {
            "$ref": "#/components/schemas/WebtunnelRecords",
            "description": "The three DNS lines the operator creates, exactly as the box prints them."
          },
          "checks": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "The four checks as the last run stored them (`address`, `dns`, `caa`, `port80`, each with a `state`); `null` until the first ran."
          },
          "checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the checks last ran."
          },
          "links_ready": {
            "type": "integer",
            "format": "int64",
            "description": "The entry's links that have a live route right now."
          },
          "links_total": {
            "type": "integer",
            "format": "int64",
            "description": "The entry's links that have a name."
          }
        }
      },
      "WebtunnelRecords": {
        "type": "object",
        "description": "The DNS lines the box prints for the operator to create. Each is `null` without what it is made of.",
        "required": [
          "a",
          "aaaa",
          "caa"
        ],
        "properties": {
          "a": {
            "type": [
              "string",
              "null"
            ],
            "description": "`*.<domain>. A <address>`"
          },
          "aaaa": {
            "type": [
              "string",
              "null"
            ],
            "description": "`*.<domain>. AAAA <address6>`"
          },
          "caa": {
            "type": [
              "string",
              "null"
            ],
            "description": "`<domain>. CAA 0 issue \"letsencrypt.org; accounturi=<the platform's account URL>\"`: the same text for every operator."
          }
        }
      },
      "RegisterNodeRequest": {
        "type": "object",
        "description": "The node to register. Only the type, the address and the country are needed; the rest has the defaults the dashboard's form has.",
        "required": [
          "node_type",
          "endpoint_host",
          "country"
        ],
        "properties": {
          "node_type": {
            "type": "string",
            "enum": [
              "origin",
              "edge"
            ],
            "description": "`origin` for an exit, `edge` for an entry."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 256,
            "description": "A name, at most 256 characters and none of `<`, `>` or `\"`."
          },
          "endpoint_host": {
            "type": "string",
            "minLength": 1,
            "description": "The server's address. Nobody else may have a node there."
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "description": "A two-letter country code."
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "A city."
          },
          "service_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "A service of the caller's to register the node under; most nodes have none."
          },
          "wg_subnet": {
            "type": [
              "string",
              "null"
            ],
            "description": "A /24 inside `10.100.0.0/16` for an exit's links, such as `10.100.13.0/24`. An exit is given the next free one when it names none."
          },
          "xray_listen_port": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The port XRay listens on. Default 443."
          },
          "reality_public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "A Reality public key. The agent reports its own."
          },
          "reality_short_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "A Reality short id. The agent reports its own."
          },
          "reality_sni": {
            "type": [
              "string",
              "null"
            ],
            "description": "The cover site's name for an exit: a host name of at least two labels with letters, digits, dots and hyphens, at most 253 characters, lower-cased."
          },
          "wg_public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "A WireGuard public key. The agent reports its own."
          },
          "wg_endpoint": {
            "type": [
              "string",
              "null"
            ],
            "description": "A WireGuard endpoint. The agent reports its own."
          },
          "ed25519_public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "A checkpoint signing key. The agent reports its own."
          },
          "price_per_gb_nano": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The price per GB in nano TON, before the platform's fee. An exit's is 0.001 to 100 TON; an entry may be 0, and is when it names none."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1024,
            "description": "A description, at most 1024 characters and none of `<`, `>` or `\"`."
          },
          "speed_mbps": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The speed declared, in Mbit/s."
          },
          "max_users": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The most users at once."
          },
          "warp_enabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Send an exit's users' TCP through Cloudflare WARP. Ignored for an entry."
          },
          "torrent_guard": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "End sessions that look like torrenting. Ignored for an entry."
          },
          "tg_max_ratio": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Torrent Guard's upload to download ratio. Default 1.5."
          },
          "tg_max_session_gb": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Torrent Guard's session size in GB; none means no limit."
          },
          "monthly_bandwidth_cap_gb": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "A monthly cap in GB; none means no cap."
          },
          "supported_tunnels": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "wg",
                "gre",
                "gre_ipsec",
                "amneziawg"
              ]
            },
            "description": "Tunnel types the node supports. Default `[\"wg\"]`."
          },
          "preferred_tunnel": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg",
              null
            ],
            "description": "The tunnel type tried first. Default `wg`."
          },
          "supported_transports": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "vless_reality",
                "hysteria2",
                "vless_xhttp",
                "olcrtc"
              ]
            },
            "description": "The transports an exit offers users. Default `[\"vless_reality\"]`."
          },
          "is_public": {
            "type": "boolean",
            "description": "`false` makes the node private: hidden from the marketplace and reachable by invitation only. Default `true`."
          }
        }
      },
      "RegisterNodeResponse": {
        "type": "object",
        "description": "A node's id, its agent token and the commands that install the agent with it.",
        "required": [
          "node_id",
          "agent_token",
          "install_command",
          "install_command_systemd"
        ],
        "properties": {
          "node_id": {
            "type": "string",
            "format": "uuid",
            "description": "The node's id."
          },
          "agent_token": {
            "type": "string",
            "description": "The token the agent connects with. It is shown here and never again: the coordinator keeps only its hash."
          },
          "install_command": {
            "type": "string",
            "description": "The command to run on the server, as root, to install the agent in Docker with that token: the installer the site serves, in container mode, with the node's type, country, SNI, tunnel, WARP and Torrent Guard settings, as the dashboard shows it. A node whose `runtime` is `systemd` (`GET /nodes/{id}` shows it) is reinstalled with `install_command_systemd` instead: this installer stops, before it changes anything, on a server that already runs the agent as a systemd service. It holds the token: handle it as a secret. If the install stops, run `curl -fsSL https://proofkit.org/install.sh | bash -s -- --check` on the server: it says what the server lacks. In revision 1.0 this field held the systemd install, which is now `install_command_systemd`."
          },
          "install_command_systemd": {
            "type": "string",
            "description": "The command to run on the server, as root, to install the agent as a systemd service instead of in Docker, with the same token: the agent binary, installed and started by systemd. It holds the token: handle it as a secret.",
            "x-since": "1.1"
          }
        }
      },
      "NodePatch": {
        "type": "object",
        "description": "The settings to change. Name only those: what is left out stays as it is, and a `null` for a typed setting is the same as leaving it out. A name the node does not have is `UNKNOWN_FIELD`.",
        "properties": {
          "price_per_gb_nano": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "A new price in nano TON per GB, before the platform's fee. An exit's is 0.001 to 100 TON; an entry may be 0. A price changes at most once in 24 hours and rises at most threefold at a time, unless the price now is 0."
          },
          "max_users": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 0,
            "maximum": 10000,
            "description": "The most users at once, 0 to 10000."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "A description, at most 500 characters and none of `<`, `>` or `\"`."
          },
          "speed_mbps": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The speed declared, in Mbit/s."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 255,
            "description": "A name, at most 255 characters and none of `<`, `>` or `\"`."
          },
          "tg_max_ratio": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "minimum": 0.1,
            "maximum": 10,
            "description": "Torrent Guard's upload to download ratio, 0.1 to 10."
          },
          "tg_max_session_gb": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "minimum": 0.1,
            "maximum": 1000,
            "description": "Torrent Guard's session size in GB, 0.1 to 1000."
          },
          "monthly_bandwidth_cap_gb": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "A monthly cap in GB."
          },
          "supported_tunnels": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "wg",
                "gre",
                "gre_ipsec",
                "amneziawg"
              ]
            },
            "description": "Tunnel types the node supports."
          },
          "preferred_tunnel": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg",
              null
            ],
            "description": "The tunnel type tried first."
          },
          "supported_transports": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "vless_reality",
                "hysteria2",
                "vless_xhttp",
                "olcrtc"
              ]
            },
            "description": "The transports an exit offers users."
          },
          "olcrtc_carriers": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "telemost",
                "wbstream",
                "mtslink",
                "salutejazz",
                "vkcalls"
              ]
            },
            "uniqueItems": true,
            "description": "The olcRTC carriers the node offers, each at most once. A carrier that is not usable on the node is `CARRIER_UNAVAILABLE`, unless the node offers it already. `null` leaves the list as it is."
          },
          "reach_checks": {
            "type": "boolean",
            "description": "Whether the node's agent runs the reachability checks: `true` or `false`, and nothing else (`INVALID_REACH_CHECKS`)."
          },
          "webtunnel": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "domain": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "address6": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "additionalProperties": false,
            "description": "An entry's WebTunnel box: `enabled` (true or false), `domain`, `address` and `address6` (text, or `null` to clear). No other key."
          },
          "reality_sni": {
            "type": [
              "string",
              "null"
            ],
            "description": "An exit's cover site name, as at registration. It may not clash with another exit's on an entry the exit is linked to (`CONFLICT`)."
          },
          "is_public": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`false` makes the node private, `true` public."
          }
        }
      },
      "NodeChecks": {
        "type": "object",
        "description": "The node's checks, judged now, and where the node is on its way to carrying users.",
        "required": [
          "node_id",
          "state",
          "checks"
        ],
        "properties": {
          "node_id": {
            "type": "string",
            "format": "uuid",
            "description": "The node."
          },
          "state": {
            "$ref": "#/components/schemas/NodeState",
            "description": "Where the node is on its way to carrying users."
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Check"
            },
            "description": "Every check of the node, with its words and its fix."
          }
        }
      },
      "Check": {
        "type": "object",
        "description": "One check of one node. A check's id, subject and status are final once published: they keep their meaning.",
        "required": [
          "id",
          "subject",
          "status",
          "blocks",
          "since",
          "title",
          "detail",
          "fix"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CheckId",
            "description": "The check."
          },
          "subject": {
            "type": "string",
            "description": "What the check is about when one id has several checks. An exit's own line is `reality`, `hysteria2` or `xhttp`; an entry's line names the exit it goes through, as `reality@<exit id>`; empty otherwise."
          },
          "status": {
            "$ref": "#/components/schemas/CheckStatus",
            "description": "What the check came to."
          },
          "blocks": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CheckBlocks"
              },
              {
                "type": "null"
              }
            ],
            "description": "`review` only while this check holds the node back from review; `null` otherwise."
          },
          "since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the check took the status it has, as the last pass of the checks recorded it, in whole seconds. A turn between `fail` and `warn` is one spell, so it keeps the start of the spell: that is when the check began to be a problem. Any other change of status starts again from the pass. `null` until a pass has recorded the status, up to one pass (5 minutes) after a change."
          },
          "title": {
            "type": "string",
            "description": "The check in a few words."
          },
          "detail": {
            "type": "string",
            "description": "What it found, in a sentence or two."
          },
          "fix": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Fix"
              },
              {
                "type": "null"
              }
            ],
            "description": "How to fix it; `null` for a check that passes."
          }
        }
      },
      "Fix": {
        "type": "object",
        "description": "How to fix a check.",
        "required": [
          "text",
          "commands",
          "help"
        ],
        "properties": {
          "text": {
            "type": "string",
            "description": "What to do and why."
          },
          "commands": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FixCommand"
            },
            "description": "Commands to run on the server, as root, in order for one shell: a `cd` comes first and what follows is relative to it. Show or run those for the node's runtime and `any` together. A command never carries a secret."
          },
          "help": {
            "type": "string",
            "description": "The help topic that explains the checks, a path of the site."
          }
        }
      },
      "FixCommand": {
        "type": "object",
        "description": "One command to run on the server, as root.",
        "required": [
          "runtime",
          "run"
        ],
        "properties": {
          "runtime": {
            "$ref": "#/components/schemas/FixRuntime",
            "description": "The engine or init system it is for."
          },
          "run": {
            "type": "string",
            "description": "The command."
          }
        }
      },
      "ChecksRun": {
        "type": "object",
        "description": "A new round of the node's test connections was asked for.",
        "required": [
          "asked"
        ],
        "properties": {
          "asked": {
            "type": "boolean",
            "description": "Always `true`: the round is dialled within a minute or so, and the checks show it when read again."
          }
        }
      },
      "MetricsHistory": {
        "type": "object",
        "description": "A node's metrics over a range, with what the charts draw them against.",
        "required": [
          "range",
          "resolution_secs",
          "capacity_mbps",
          "conntrack_max",
          "nproc",
          "thresholds",
          "points"
        ],
        "properties": {
          "range": {
            "type": "string",
            "description": "The range that was read."
          },
          "resolution_secs": {
            "type": "integer",
            "format": "int64",
            "description": "The seconds one point stands for."
          },
          "capacity_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "The node's capacity in Mbit/s, for the charts; `null` when it is not known."
          },
          "conntrack_max": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The connection table's size, which the charts read `conntrack` against; `null` when it is not known."
          },
          "nproc": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The node's CPU cores; `null` when it is not known."
          },
          "thresholds": {
            "type": "object",
            "additionalProperties": true,
            "description": "The overload rule's levels, which the charts draw: `mem` in percent, `load_per_cpu` (the five-minute load per CPU core), `conntrack_pct` (how full the connection table is, in percent) and `listen_overflows` (of one heartbeat)."
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetricsPoint"
            },
            "description": "The points, oldest first."
          }
        }
      },
      "MetricsPoint": {
        "type": "object",
        "description": "One interval of a node's metrics. A figure the agent did not report in the interval is `null`.",
        "required": [
          "ts",
          "users",
          "down_mbps",
          "up_mbps",
          "cpu",
          "mem",
          "load",
          "connections",
          "disk_free_mb",
          "overloaded",
          "uplink_rx_mbps",
          "uplink_tx_mbps",
          "ping_ms",
          "ping_jitter_ms",
          "latency_ms",
          "jitter_ms",
          "conntrack",
          "conntrack_full_drops",
          "listen_overflows"
        ],
        "properties": {
          "ts": {
            "type": "number",
            "format": "double",
            "description": "The interval's start, in seconds since the Unix epoch."
          },
          "users": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Users on the node."
          },
          "down_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Download, in Mbit/s."
          },
          "up_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Upload, in Mbit/s."
          },
          "cpu": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "What the agent reports as `cpu`, which is its load average and not a percentage."
          },
          "mem": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Memory use, in percent."
          },
          "load": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The five-minute load average. The overload rule compares it with the cores times `thresholds.load_per_cpu`."
          },
          "connections": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Open connections."
          },
          "disk_free_mb": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Free disk space, in MiB."
          },
          "overloaded": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Share of the interval the node read as overloaded."
          },
          "uplink_rx_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "The uplink's receive rate, in Mbit/s."
          },
          "uplink_tx_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "The uplink's transmit rate, in Mbit/s."
          },
          "ping_ms": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Ping, in milliseconds."
          },
          "ping_jitter_ms": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "Ping jitter, in milliseconds."
          },
          "latency_ms": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The same value as `ping_ms`."
          },
          "jitter_ms": {
            "type": [
              "number",
              "null"
            ],
            "format": "float",
            "description": "The same value as `ping_jitter_ms`."
          },
          "conntrack": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The connection table's peak count in the interval (agent 0.4.20 and later)."
          },
          "conntrack_full_drops": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Connections dropped for want of room in the table, summed over the interval (agent 0.4.23 and later)."
          },
          "listen_overflows": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Connections dropped at a full listen queue, summed over the interval (agent 0.4.23 and later)."
          }
        }
      },
      "NodeEvent": {
        "type": "object",
        "description": "One event of a node: agent starts and version changes, overload windows, links up and down, olcRTC carrier events.",
        "required": [
          "at",
          "kind",
          "detail"
        ],
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened."
          },
          "kind": {
            "type": "string",
            "description": "What happened, as a word such as `overload_start`."
          },
          "detail": {
            "type": "object",
            "additionalProperties": true,
            "description": "Facts about it; the keys depend on `kind`."
          }
        }
      },
      "Restart": {
        "type": "object",
        "description": "The restart command was sent.",
        "required": [
          "sent"
        ],
        "properties": {
          "sent": {
            "type": "boolean",
            "description": "Always `true`: the command went out over the control channel, which does not mean the agent has restarted yet."
          }
        }
      },
      "Link": {
        "type": "object",
        "description": "A link between an entry and an exit, with both ends' names and its health.",
        "required": [
          "id",
          "edge_id",
          "origin_id",
          "wg_edge_ip",
          "wg_origin_ip",
          "priority",
          "status",
          "tunnel_type",
          "created_at",
          "updated_at",
          "edge_name",
          "edge_country",
          "edge_endpoint_host",
          "origin_name",
          "origin_country",
          "origin_endpoint_host",
          "edge_supported_tunnels",
          "origin_supported_tunnels",
          "active_sessions",
          "edge_is_mine",
          "origin_is_mine",
          "health"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The link's id."
          },
          "edge_id": {
            "type": "string",
            "format": "uuid",
            "description": "The entry."
          },
          "origin_id": {
            "type": "string",
            "format": "uuid",
            "description": "The exit."
          },
          "wg_edge_ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's address inside the tunnel."
          },
          "wg_origin_ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's address inside the tunnel."
          },
          "priority": {
            "type": "integer",
            "format": "int32",
            "description": "The link's priority among the entry's."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "broken"
            ],
            "description": "The link's status."
          },
          "tunnel_type": {
            "type": "string",
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg"
            ],
            "description": "The tunnel the link uses."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the link was made."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it last changed."
          },
          "edge_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's name when the entry is yours. When it is another operator's (`edge_is_mine` is false) it is our own label for it, `RU entry 05568fa5`: its country, `entry` and the first eight characters of its id, never the name its owner gave it."
          },
          "edge_country": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's country."
          },
          "edge_endpoint_host": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's address when the entry is yours. When it is another operator's (`edge_is_mine` is false) it is its address only if that is an IP address, IPv4 or IPv6, and `null` for anything else its owner registered it as: a host name or other text is its owner's words."
          },
          "origin_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's name when the exit is yours. When it is another operator's (`origin_is_mine` is false) it is our own label for it, `DE exit 05568fa5`: its country, `exit` and the first eight characters of its id, never the name its owner gave it."
          },
          "origin_country": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's country."
          },
          "origin_endpoint_host": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's address when the exit is yours. When it is another operator's (`origin_is_mine` is false) it is its address only if that is an IP address, IPv4 or IPv6, and `null` for anything else its owner registered it as: a host name or other text is its owner's words."
          },
          "edge_supported_tunnels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "wg",
                "gre",
                "gre_ipsec",
                "amneziawg"
              ]
            },
            "description": "The tunnel types the entry supports."
          },
          "origin_supported_tunnels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "wg",
                "gre",
                "gre_ipsec",
                "amneziawg"
              ]
            },
            "description": "The tunnel types the exit supports."
          },
          "active_sessions": {
            "type": "integer",
            "format": "int64",
            "description": "Sessions carried by this entry and exit pair now: what removing the link closes."
          },
          "edge_is_mine": {
            "type": "boolean",
            "description": "Whether the entry is the caller's."
          },
          "origin_is_mine": {
            "type": "boolean",
            "description": "Whether the exit is the caller's."
          },
          "health": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LinkHealth"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the link carried in its last 5-minute bucket, its loss over the last hour and a 24 hour sparkline; `null` before its first bucket."
          }
        }
      },
      "LinkHealth": {
        "type": "object",
        "description": "A link's traffic and loss.",
        "required": [
          "down_mbps",
          "up_mbps",
          "last_bucket_at",
          "loss_down_pct",
          "loss_up_pct",
          "spark"
        ],
        "properties": {
          "down_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Download, exit to entry, from the newest bucket; `null` when the bucket is older than the tunnel is expected to report."
          },
          "up_mbps": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Upload, entry to exit, from the newest bucket; `null` under the same rule."
          },
          "last_bucket_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the newest bucket was written."
          },
          "loss_down_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Loss in the download direction over the last hour; `null` for a direction that sent under 50 MB."
          },
          "loss_up_pct": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Loss in the upload direction over the last hour; `null` for a direction that sent under 50 MB."
          },
          "spark": {
            "type": "array",
            "items": {
              "type": "number",
              "format": "float"
            },
            "description": "A 24 hour sparkline of the link's traffic."
          }
        }
      },
      "CreateLinkRequest": {
        "type": "object",
        "description": "The two nodes to link: the caller's entry and an exit.",
        "required": [
          "edge_id",
          "origin_id"
        ],
        "properties": {
          "edge_id": {
            "type": "string",
            "format": "uuid",
            "description": "The caller's own entry."
          },
          "origin_id": {
            "type": "string",
            "format": "uuid",
            "description": "The exit: the caller's, or another operator's that admits foreign entries."
          },
          "tunnel_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg",
              null
            ],
            "description": "The tunnel to use; chosen from what both nodes support when left out."
          }
        }
      },
      "LinkResponse": {
        "type": "object",
        "description": "The link that was made.",
        "required": [
          "id",
          "edge_id",
          "origin_id",
          "wg_edge_pubkey",
          "wg_origin_pubkey",
          "wg_edge_ip",
          "wg_origin_ip",
          "priority",
          "status",
          "tunnel_type",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The link's id."
          },
          "edge_id": {
            "type": "string",
            "format": "uuid",
            "description": "The entry."
          },
          "origin_id": {
            "type": "string",
            "format": "uuid",
            "description": "The exit."
          },
          "wg_edge_pubkey": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's WireGuard public key for the link. The entry is always yours."
          },
          "wg_origin_pubkey": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's WireGuard public key for the link when the exit is yours. When it is another operator's the key is its owner's text and is `null` here; the link is made all the same."
          },
          "wg_edge_ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's address inside the tunnel."
          },
          "wg_origin_ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exit's address inside the tunnel."
          },
          "priority": {
            "type": "integer",
            "format": "int32",
            "description": "The link's priority."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "broken"
            ],
            "description": "The link's status."
          },
          "tunnel_type": {
            "type": "string",
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg"
            ],
            "description": "The tunnel the link uses."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the link was made."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it last changed."
          }
        }
      },
      "SwitchTunnelRequest": {
        "type": "object",
        "description": "The tunnel type to switch the link to.",
        "required": [
          "tunnel_type"
        ],
        "properties": {
          "tunnel_type": {
            "type": "string",
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg"
            ],
            "description": "The tunnel type. Both nodes must support it."
          }
        }
      },
      "Invitation": {
        "type": "object",
        "description": "An invitation to a private node. The token is shown once, when the invitation is made; this has only its first characters.",
        "required": [
          "id",
          "token_prefix",
          "label",
          "max_uses",
          "used_count",
          "expires_at",
          "revoked_at",
          "created_at",
          "active_grants"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The invitation's id."
          },
          "token_prefix": {
            "type": "string",
            "description": "The first characters of the token."
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "The label given when it was made."
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "How many times it may be used; `null` for no limit."
          },
          "used_count": {
            "type": "integer",
            "format": "int32",
            "description": "How many times it has been used."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it stops working; `null` for never."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was revoked; `null` while it stands."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it was made."
          },
          "active_grants": {
            "type": "integer",
            "format": "int64",
            "description": "Accounts that hold access through it now."
          }
        }
      },
      "CreateInvitationRequest": {
        "type": "object",
        "description": "The limits of an invitation. A body with nothing in it makes one with no label, no limit and no expiry.",
        "properties": {
          "label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100,
            "description": "A label so you know who it is for, at most 100 characters."
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "minimum": 1,
            "description": "How many times it may be used, 1 or more."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it stops working, in the future."
          }
        }
      },
      "CreateInvitationResponse": {
        "type": "object",
        "description": "The invitation that was made, with its token.",
        "required": [
          "id",
          "token",
          "magic_link",
          "token_prefix",
          "label",
          "max_uses",
          "used_count",
          "expires_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The invitation's id."
          },
          "token": {
            "type": "string",
            "description": "The token. It is shown here and never again: the coordinator keeps only its hash."
          },
          "magic_link": {
            "type": "string",
            "description": "A link that opens the invitation on the site."
          },
          "token_prefix": {
            "type": "string",
            "description": "The first characters of the token, as the list shows them."
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "The label."
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "How many times it may be used; `null` for no limit."
          },
          "used_count": {
            "type": "integer",
            "format": "int32",
            "description": "Always 0 when it is made."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it stops working; `null` for never."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it was made."
          }
        }
      },
      "UpdateNodeAnswer": {
        "type": "object",
        "description": "The change was made.",
        "required": [
          "status",
          "node_id"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "updated"
            ],
            "description": "Always `updated`."
          },
          "node_id": {
            "type": "string",
            "format": "uuid",
            "description": "The node that was changed."
          },
          "webtunnel": {
            "$ref": "#/components/schemas/WebtunnelBox",
            "description": "The node's WebTunnel box as it reads now, when the change named it."
          }
        }
      },
      "DeleteNodeAnswer": {
        "type": "object",
        "description": "The node was deleted and its stake refunded.",
        "required": [
          "id",
          "deleted"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The deleted node."
          },
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Always `true`."
          }
        }
      },
      "Benchmark": {
        "type": "object",
        "description": "A speed test of the node.",
        "required": [
          "node_id",
          "down_mbps",
          "up_mbps",
          "latency_ms",
          "benchmark_at"
        ],
        "properties": {
          "node_id": {
            "type": "string",
            "format": "uuid",
            "description": "The node that was tested."
          },
          "down_mbps": {
            "type": "number",
            "format": "float",
            "description": "Download, in Mbit/s."
          },
          "up_mbps": {
            "type": "number",
            "format": "float",
            "description": "Upload, in Mbit/s."
          },
          "latency_ms": {
            "type": "number",
            "format": "float",
            "description": "Latency, in milliseconds."
          },
          "benchmark_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the test ran."
          }
        }
      },
      "DeleteLinkAnswer": {
        "type": "object",
        "description": "The link was broken.",
        "required": [
          "id",
          "status",
          "closed_sessions"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The link."
          },
          "status": {
            "type": "string",
            "enum": [
              "broken"
            ],
            "description": "Always `broken`."
          },
          "closed_sessions": {
            "type": "integer",
            "format": "int64",
            "description": "How many sessions on the link were closed."
          }
        }
      },
      "SwitchTunnelAnswer": {
        "type": "object",
        "description": "The link's tunnel was switched, or already was the one asked for.",
        "required": [
          "id",
          "tunnel_type",
          "switched"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The link."
          },
          "tunnel_type": {
            "type": "string",
            "enum": [
              "wg",
              "gre",
              "gre_ipsec",
              "amneziawg"
            ],
            "description": "The link's tunnel type now."
          },
          "switched": {
            "type": "boolean",
            "description": "`false` when the link already used that tunnel type and nothing was done."
          },
          "message": {
            "type": "string",
            "description": "Present when `switched` is `false`: why nothing was done."
          },
          "closed_sessions": {
            "type": "integer",
            "format": "int64",
            "description": "Present when `switched` is `true`: how many sessions on the link were closed."
          }
        }
      },
      "RevokeInvitationAnswer": {
        "type": "object",
        "description": "The invitation was revoked, or its grants were.",
        "required": [
          "ok",
          "grants_revoked"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Always `true`."
          },
          "grants_revoked": {
            "type": "integer",
            "format": "int64",
            "description": "How many grants were revoked with it (`revoke_grants=true`), otherwise 0."
          }
        }
      },
      "CheckId": {
        "type": "string",
        "description": "The id of a check. A check's id, subject and status are published: once in this contract they keep their meaning.",
        "enum": [
          "agent_never_connected",
          "agent_offline",
          "agent_outdated",
          "disk_low",
          "cover_site",
          "transport_unreachable",
          "exit_probe_failing",
          "host_tuning",
          "overloaded",
          "entry_unlinked",
          "carrier_account_error",
          "price_above_ceiling",
          "in_review",
          "paused",
          "suspended"
        ]
      },
      "CheckStatus": {
        "type": "string",
        "description": "What a check came to. `pending` is no data yet, or our own probe is suspect; it never notifies.",
        "enum": [
          "ok",
          "warn",
          "fail",
          "info",
          "pending"
        ]
      },
      "CheckBlocks": {
        "type": "string",
        "description": "What a check holds back while its status says so: only the node's review.",
        "enum": [
          "review"
        ]
      },
      "FixRuntime": {
        "type": "string",
        "description": "The engine or init system a command is for; `any` runs on whichever the node uses.",
        "enum": [
          "docker",
          "podman",
          "systemd",
          "any"
        ]
      },
      "NodeState": {
        "type": "string",
        "description": "Where a node is on its way to carrying users, judged from its facts and its checks, never stored.",
        "enum": [
          "registered",
          "checking",
          "in_review",
          "live",
          "not_routed",
          "paused",
          "offline",
          "suspended"
        ]
      },
      "KeyScope": {
        "type": "string",
        "description": "What a key may do: `read` reads, `manage` also changes. Every key has `read`, and `manage` implies it.",
        "enum": [
          "read",
          "manage"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The key is not good: missing, malformed, unknown, expired or revoked. They are deliberately not told apart, and a wrong secret behind a known prefix does not spend that key's budget. A signed-in session that is missing or not valid gets `UNAUTHORIZED`. A public route answers anyone, but a key sent with it must be good.\n\nCodes: `INVALID_OPERATOR_KEY`, `UNAUTHORIZED`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "INVALID_OPERATOR_KEY": {
                "summary": "Invalid, expired or revoked operator API key",
                "value": {
                  "error": {
                    "code": "INVALID_OPERATOR_KEY",
                    "message": "Invalid, expired or revoked operator API key"
                  }
                }
              },
              "SESSION": {
                "summary": "A session's access token that is missing or not valid, or whose account is gone",
                "value": {
                  "error": {
                    "code": "UNAUTHORIZED",
                    "message": "Missing token"
                  }
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The call is understood and refused. A key without the `manage` scope may read but not change (`INSUFFICIENT_SCOPE`). A signed-in session must be an operator's: any other role, an admin's included, is `FORBIDDEN`, because this contract has no admin bypass. A key only ever acts as its own operator.\n\nCodes: `INSUFFICIENT_SCOPE`, `FORBIDDEN`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "INSUFFICIENT_SCOPE": {
                "summary": "This key cannot manage nodes. Make a key with the manage scope.",
                "value": {
                  "error": {
                    "code": "INSUFFICIENT_SCOPE",
                    "message": "This key cannot manage nodes. Make a key with the manage scope."
                  }
                }
              },
              "SESSION": {
                "summary": "Operator access required",
                "value": {
                  "error": {
                    "code": "FORBIDDEN",
                    "message": "Operator access required"
                  }
                }
              }
            }
          }
        }
      },
      "UnknownEndpoint": {
        "description": "Any path under `/api/v1/operator` that this contract does not list, and, word for word, every route while the API is not open to the calling account: a switch that is off reads as routes that are not there. Another operator's resource is not this answer: it is the resource's own 404.\n\nCodes: `NOT_FOUND`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "NOT_FOUND": {
                "summary": "Unknown operator API endpoint",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Unknown operator API endpoint"
                  }
                }
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "A path the contract has, under a method it does not take.\n\nCodes: `METHOD_NOT_ALLOWED`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "METHOD_NOT_ALLOWED": {
                "summary": "This operator API endpoint does not support that method",
                "value": {
                  "error": {
                    "code": "METHOD_NOT_ALLOWED",
                    "message": "This operator API endpoint does not support that method"
                  }
                }
              }
            }
          }
        }
      },
      "NodeNotFound": {
        "description": "Not your node, or not a node: one answer for both, so the contract tells nobody which nodes exist. The same 404 as the API being closed to your account.\n\nCodes: `NOT_FOUND`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "NODE_NOT_FOUND": {
                "summary": "A node of another operator, an id nobody has, or an id that is no node id",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Node not found"
                  }
                }
              },
              "API_NOT_OPEN": {
                "summary": "The API is not open to this account",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Unknown operator API endpoint"
                  }
                }
              }
            }
          }
        }
      },
      "LinkNotFound": {
        "description": "Not your link, or not a link: one answer for both, so the contract tells nobody which links exist. The same 404 as the API being closed to your account.\n\nCodes: `NOT_FOUND`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "LINK_NOT_FOUND": {
                "summary": "A link between two other operators' nodes, an id nobody has, or an id that is no link id",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Link not found"
                  }
                }
              },
              "API_NOT_OPEN": {
                "summary": "The API is not open to this account",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Unknown operator API endpoint"
                  }
                }
              }
            }
          }
        }
      },
      "InvitationNotFound": {
        "description": "Not your node, or not an invitation of it.\n\nCodes: `NOT_FOUND`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "NODE_NOT_FOUND": {
                "summary": "Node not found",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Node not found"
                  }
                }
              },
              "INVITATION_NOT_FOUND": {
                "summary": "An invitation the node does not have, or an id that is no invitation id",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Invitation not found"
                  }
                }
              },
              "API_NOT_OPEN": {
                "summary": "The API is not open to this account",
                "value": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "Unknown operator API endpoint"
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limited. A key has 300 calls a minute, and 60 of them may be writes (every method but GET and HEAD), counted in fixed 60-second windows for the key whichever APP serves the call. A session has no per-key budget. Some operations have a budget of their own, said where they are described (a node's restarts, its benchmark and its Check again, an operator's invitations), and refuse with the same code. Every `429` the API gives carries `Retry-After`, the seconds that are left of the window that refused the call. A `429` with **no** `X-RateLimit-*` headers and no `X-Operator-Api-Version` comes from the per-IP flood guard in front of the API (600 a minute per address), which runs first: it carries no `Retry-After`, and its window is a minute.\n\nCodes: `RATE_LIMITED`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "RATE_LIMITED": {
                "summary": "Rate limit exceeded: 300 requests per 60 seconds",
                "value": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Rate limit exceeded: 300 requests per 60 seconds"
                  }
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Our failure. If the rate limiter's store is unreachable, the per-IP layer in front answers this with \"Rate limiter unavailable\", without the `X-RateLimit-*` headers and the revision.\n\nCodes: `INTERNAL_ERROR`.",
        "headers": {
          "X-Operator-Api-Version": {
            "$ref": "#/components/headers/X-Operator-Api-Version"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/X-Request-ID"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "INTERNAL_ERROR": {
                "summary": "Database error",
                "value": {
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "Database error"
                  }
                }
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "A body over 1 MB. Our proxy refuses it before it reaches the API, so the answer is its HTML page: no JSON envelope, no `X-RateLimit-*`, no `X-Request-ID`, no revision. Every body this API takes is far smaller. A body over 2 MB that got past the proxy is the coordinator's own `413 PAYLOAD_TOO_LARGE` in the envelope.\n\nCodes: `PAYLOAD_TOO_LARGE`.",
        "content": {
          "text/html": {
            "schema": {
              "type": "string"
            },
            "example": "<html>\r\n<head><title>413 Request Entity Too Large</title></head>\r\n<body>\r\n<center><h1>413 Request Entity Too Large</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"
          },
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "PAYLOAD_TOO_LARGE": {
                "summary": "The coordinator's own refusal",
                "value": {
                  "error": {
                    "code": "PAYLOAD_TOO_LARGE",
                    "message": "The request body is too large"
                  }
                }
              }
            }
          }
        }
      }
    },
    "parameters": {
      "NodeId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A node of yours. A node that is another operator's, a node that does not exist and an id that is no node id all answer `404 NOT_FOUND` \"Node not found\", with one body.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "0e5a1c2b-7d3f-4a9e-8b6c-000000000001"
      },
      "LinkId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A link that ends at a node of yours. A link between two other operators' nodes, a link that does not exist and an id that is no link id all answer `404 NOT_FOUND` \"Link not found\", with one body.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "0e5a1c2b-7d3f-4a9e-8b6c-000000000031"
      },
      "InvitationId": {
        "name": "iid",
        "in": "path",
        "required": true,
        "description": "An invitation of the node, from `GET /nodes/{id}/invitations`.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "0e5a1c2b-7d3f-4a9e-8b6c-000000000041"
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Makes a registration safe to retry. 1 to 255 visible ASCII characters, no spaces; a UUID is a good one. The registration runs once for an account and key, and the first answer is kept for 24 hours. A retry with the same key and the same body gets that answer again, with `Idempotent-Replayed: true`. A key given twice in one request is `INVALID_IDEMPOTENCY_KEY`. The explorer sends no key of its own: write one if you want it.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      }
    },
    "headers": {
      "X-Operator-Api-Version": {
        "description": "The revision of the contract that produced this answer, as `1.N`. On every answer of the contract, a refusal and a fallback included. Absent on a `429` or `500` from the per-IP layer in front of it, and on the proxy's `413`.",
        "schema": {
          "type": "string",
          "pattern": "^1\\.\\d+$"
        },
        "example": "1.0"
      },
      "X-RateLimit-Limit": {
        "description": "The budget this answer shows, per minute: 300 calls, or 60 for a write while its write budget has fewer calls left than the whole one. On every answer to a call made with a key that was counted. Absent on a session's call, on the `401` for a key that was refused, on the `404` of an account the API is not open to, and on the answers of the per-IP layer.",
        "schema": {
          "type": "integer"
        },
        "example": 300
      },
      "X-RateLimit-Remaining": {
        "description": "Calls left in the current 60-second window of the budget `X-RateLimit-Limit` names. Present where that header is.",
        "schema": {
          "type": "integer"
        },
        "example": 297
      },
      "X-RateLimit-Reset": {
        "description": "Seconds until that window resets. Present where `X-RateLimit-Limit` is.",
        "schema": {
          "type": "integer"
        },
        "example": 42
      },
      "X-Request-ID": {
        "description": "Our id for this request; send your own and we echo it. Quote it when you contact us about a call.",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before asking again: what is left of the window that refused the call. On a `429` for a key it equals `X-RateLimit-Reset`. On a limit of a node or an operator it is what is left of that limit's own window: an hour for a restart, 5 minutes for Check again, a minute for a benchmark and for invitations.",
        "schema": {
          "type": "integer"
        },
        "example": 42
      },
      "Idempotent-Replayed": {
        "description": "On a `201` that is the first answer of the same `Idempotency-Key` given again: nothing was registered this time.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    }
  }
}
