{
  "openapi": "3.0.3",
  "info": {
    "title": "Oya Browser API",
    "description": "REST API for controlling browsers connected to the Oya Browser server. Browsers connect via WebSocket; AI tools and clients use this REST API (or MCP) to send commands.",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://oyabrowser.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:3100",
      "description": "Local development"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/control": {
      "get": {
        "summary": "Project operational overview",
        "operationId": "getApiControl",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        }
      }
    },
    "/api/control/sessions": {
      "get": {
        "summary": "Durable inventory including unresolved cleanup",
        "operationId": "getApiControlSessions",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ControlSession"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        }
      },
      "post": {
        "summary": "Create a durable browser session",
        "operationId": "postApiControlSessions",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          },
          "201": {
            "description": "Provisioning accepted"
          },
          "202": {
            "description": "Queued or existing operation; poll by returned ID"
          },
          "429": {
            "description": "Capacity or budget exhausted"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider": {
                    "type": "string"
                  },
                  "persona": {
                    "type": "string"
                  },
                  "governed": {
                    "type": "boolean"
                  },
                  "queueMs": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 300000
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "normal",
                      "high"
                    ]
                  },
                  "budgetUsd": {
                    "type": "number"
                  },
                  "policy": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "Stable per logical creation, retained seven days; changing the body conflicts."
          }
        ]
      }
    },
    "/api/control/sessions/{id}": {
      "get": {
        "summary": "Read a durable session",
        "operationId": "getApiControlSessionsId",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlSession"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/control/sessions/{id}/cancel": {
      "post": {
        "summary": "Stop a session in any state; answers its final state",
        "operationId": "postApiControlSessionsIdCancel",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        }
      }
    },
    "/api/control/sessions/{id}/stop": {
      "post": {
        "summary": "Stop browser; optionally proceed after profile-save failure",
        "operationId": "postApiControlSessionsIdStop",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/sessions/{id}/recover": {
      "post": {
        "summary": "Recover original session or explicitly replace after cleanup",
        "operationId": "postApiControlSessionsIdRecover",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "replace": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/sessions/{id}/control": {
      "post": {
        "summary": "Transfer human control; release leaves agent paused",
        "operationId": "postApiControlSessionsIdControl",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "acquire",
                      "release",
                      "resume"
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/sessions/{id}/input": {
      "post": {
        "summary": "Send input using the caller\u2019s human control lease",
        "operationId": "postApiControlSessionsIdInput",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string"
                  },
                  "params": {
                    "type": "object"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/sessions/{id}/ticket": {
      "post": {
        "summary": "Issue a one-use 60-second session-bound ticket",
        "operationId": "postApiControlSessionsIdTicket",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        }
      }
    },
    "/api/control/project": {
      "patch": {
        "summary": "Update limits, rates, retention and admission policy",
        "operationId": "patchApiControlProject",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "maxConcurrent": {
                    "type": "integer",
                    "nullable": true
                  },
                  "budgetUsd": {
                    "type": "number",
                    "nullable": true
                  },
                  "rates": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "policy": {
                    "type": "object"
                  },
                  "recordingDays": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "auditDays": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/credentials": {
      "post": {
        "summary": "Create a credential; secret shown once",
        "operationId": "postApiControlCredentials",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "operator",
                      "administrator"
                    ]
                  },
                  "label": {
                    "type": "string"
                  },
                  "expiresAt": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/credentials/{id}": {
      "delete": {
        "summary": "Revoke credential and invalidate its attachments",
        "operationId": "deleteApiControlCredentialsId",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/control/members": {
      "get": {
        "summary": "List project members",
        "operationId": "getApiControlMembers",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential."
      }
    },
    "/api/control/members/invite": {
      "post": {
        "summary": "Create one-use seven-day invitation",
        "operationId": "postApiControlMembersInvite",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "operator",
                      "administrator"
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/members/{id}": {
      "delete": {
        "summary": "Remove member and revoke linked credentials",
        "operationId": "deleteApiControlMembersId",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/control/events": {
      "get": {
        "summary": "Read durable events after a cursor",
        "operationId": "getApiControlEvents",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ]
      }
    },
    "/api/control/audit/export": {
      "get": {
        "summary": "Export retained audit events as NDJSON",
        "operationId": "getApiControlAuditExport",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Retained audit events",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential."
      }
    },
    "/api/control/webhooks": {
      "post": {
        "summary": "Create signed at-least-once event subscription",
        "operationId": "postApiControlWebhooks",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/control/webhooks/{id}": {
      "delete": {
        "summary": "Disable event subscription",
        "operationId": "deleteApiControlWebhooksId",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/control/deliveries/{id}/replay": {
      "post": {
        "summary": "Replay a delivery within its audit retention period",
        "operationId": "postApiControlDeliveriesIdReplay",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "description": "Requires an administrator credential.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/projects": {
      "get": {
        "summary": "List projects for authenticated account",
        "operationId": "getApiAuthProjects",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        }
      }
    },
    "/api/auth/projects/join": {
      "post": {
        "summary": "Redeem a project invitation using account JWT",
        "operationId": "postApiAuthProjectsJoin",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/projects/{id}/access": {
      "post": {
        "summary": "Issue one-hour member-linked console credential using account JWT",
        "operationId": "postApiAuthProjectsIdAccess",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        }
      }
    },
    "/livez": {
      "get": {
        "summary": "Process liveness",
        "operationId": "getLivez",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "security": []
      }
    },
    "/readyz": {
      "get": {
        "summary": "Storage and admission readiness",
        "operationId": "getReadyz",
        "tags": [
          "Durable control"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked credential"
          },
          "403": {
            "description": "Insufficient role"
          },
          "409": {
            "description": "Lifecycle, idempotency, or control lease conflict"
          },
          "503": {
            "description": "Storage or session owner unavailable"
          }
        },
        "security": []
      }
    },
    "/health": {
      "get": {
        "summary": "Health check",
        "operationId": "getHealth",
        "security": [],
        "responses": {
          "200": {
            "description": "Server status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "browsers": { "type": "integer", "description": "Number of connected browsers" },
                    "uptime": { "type": "number", "description": "Server uptime in seconds" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/browsers": {
      "get": {
        "summary": "List connected browsers",
        "description": "Returns browsers connected with the caller's API key. Admin keys see all browsers.",
        "operationId": "listBrowsers",
        "responses": {
          "200": {
            "description": "List of connected browsers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Browser" }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/browsers/start": {
      "post": {
        "summary": "Start a browser",
        "description": "Starts a browser as one of the caller's personas: on the key's provider, in a sandbox, or on a CDP endpoint the caller names (any Chrome with --remote-debugging-port). Answers 201 with the browser's id and its gateway CDP URL. A second start on an endpoint another browser already holds answers 409 endpoint_in_use; the body names the holder's id when it is yours.",
        "operationId": "startBrowser",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "persona": { "type": "string", "description": "A persona id, or \"auto\" for a fresh one." },
                  "provider": { "type": "string", "description": "cdp, oya-cloud, oya-selfhosted, or a vendor; defaults to the key's configured provider." },
                  "wsUrl": { "type": "string", "description": "For provider cdp: the ws://, wss:// or http:// address of the debugging port." },
                  "name": { "type": "string", "description": "A display name for the browser." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The browser is ready to drive.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "provider": { "type": "string" },
                    "persona": { "type": "string" },
                    "status": { "type": "string" },
                    "cdpUrl": { "type": "string", "description": "The gateway CDP URL for this browser." }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field was malformed; the body names it.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "409": {
            "description": "Endpoint already held by a running browser (endpoint_in_use), or the session may not go on.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "The key or persona is at its browser quota.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "502": {
            "description": "The provider or endpoint could not be reached.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/browsers/connect": {
      "post": {
        "summary": "Attach a CDP browser",
        "description": "Attaches a browser the server dials out to, with no persona: any Chrome with --remote-debugging-port, or a vendor session. A second attach on an endpoint another browser already holds answers 409 endpoint_in_use; the body names the holder's id when it is yours.",
        "operationId": "connectBrowser",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "wsUrl": { "type": "string", "description": "The ws://, wss:// or http:// address of the debugging port." },
                  "provider": { "type": "string", "description": "cdp (default) or a vendor name." },
                  "name": { "type": "string", "description": "A display name for the browser." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Attached.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "provider": { "type": "string" },
                    "clientType": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field was malformed; the body names it.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "409": {
            "description": "Endpoint already held by a running browser (endpoint_in_use).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "The key is at its browser quota.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "502": {
            "description": "Could not attach to the CDP browser.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/browsers/{browserId}/command": {
      "post": {
        "summary": "Send a command to a browser",
        "description": "Execute a browser action (analyze, navigate, click, type, press_key, screenshot, scroll, wait, list_tabs, open_tab, switch_tab, close_tab, read_page).",
        "operationId": "sendCommand",
        "parameters": [
          { "$ref": "#/components/parameters/browserId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["action"],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "analyze",
                      "navigate",
                      "click",
                      "type",
                      "press_key",
                      "screenshot",
                      "scroll",
                      "wait",
                      "read_page",
                      "list_tabs",
                      "open_tab",
                      "switch_tab",
                      "close_tab"
                    ],
                    "description": "The browser action to execute. Each action uses only specific params (see examples)."
                  },
                  "params": {
                    "type": "object",
                    "description": "Action-specific parameters. Only include params relevant to the chosen action.",
                    "properties": {
                      "url": { "type": "string", "description": "URL (for navigate, open_tab)" },
                      "selector": { "type": "string", "description": "CSS selector, e.g. [data-ac-id=\"3\"] (for click, type, wait, read_page)" },
                      "text": { "type": "string", "description": "Text to type (for type)" },
                      "key": { "type": "string", "description": "Key to press (for press_key): Enter, Escape, Tab, Backspace, ArrowDown, ArrowUp, etc." },
                      "direction": { "type": "string", "enum": ["up", "down"], "description": "Scroll direction (for scroll)" },
                      "amount": { "type": "number", "description": "Pixels to scroll (for scroll, default 500)" },
                      "timeout": { "type": "number", "description": "Timeout in ms (for wait, default 10000)" },
                      "tab_id": { "type": "number", "description": "Tab ID (for switch_tab, close_tab)" },
                      "limit": { "type": "number", "description": "Max elements (for read_page, default 50)" }
                    }
                  }
                },
                "examples": [
                  { "action": "navigate", "params": { "url": "https://google.com" } },
                  { "action": "analyze" },
                  { "action": "click", "params": { "selector": "[data-ac-id=\"3\"]" } },
                  { "action": "type", "params": { "selector": "[data-ac-id=\"9\"]", "text": "hello world" } },
                  { "action": "press_key", "params": { "key": "Enter" } },
                  { "action": "scroll", "params": { "direction": "down", "amount": 500 } },
                  { "action": "screenshot" },
                  { "action": "list_tabs" },
                  { "action": "open_tab", "params": { "url": "https://gmail.com" } },
                  { "action": "switch_tab", "params": { "tab_id": 2 } },
                  { "action": "close_tab", "params": { "tab_id": 3 } },
                  { "action": "wait", "params": { "selector": ".results", "timeout": 10000 } },
                  { "action": "read_page", "params": { "limit": 20 } }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Command result",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CommandResult" }
              }
            }
          },
          "400": {
            "description": "Missing action",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "Browser not connected",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "500": {
            "description": "Server fault, not the request: the body carries a ref that matches the server's log line",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/browsers/{browserId}/chat": {
      "post": {
        "summary": "Chat with LLM-controlled browser",
        "description": "Send a conversation to the LLM which uses browser tools to complete tasks. The LLM autonomously calls analyze_page, click, type, etc. until the task is done.",
        "operationId": "chat",
        "parameters": [
          { "$ref": "#/components/parameters/browserId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["messages"],
                "properties": {
                  "messages": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["role", "content"],
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": ["user", "assistant", "system"]
                        },
                        "content": { "type": "string" }
                      }
                    },
                    "description": "OpenAI-format chat messages"
                  },
                  "data": {
                    "type": "object",
                    "description": "Task values the agent may read, referred to as {{name}} in the prompt; a value may be a file() to upload"
                  },
                  "secrets": {
                    "type": "object",
                    "description": "Task values the agent never sees; typed through {{name}} placeholders"
                  },
                  "schema": {
                    "type": "object",
                    "description": "A JSON schema for the answer. The agent then finishes by returning data in this shape, answered as `data`"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chat response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": { "type": "string", "description": "LLM's final text response" },
                    "toolCalls": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "args": { "type": "object" }
                        }
                      },
                      "description": "Tools the LLM called during execution"
                    },
                    "replayable": {
                      "type": "boolean",
                      "description": "Whether the run acted on a page and can be saved as a playbook (POST /api/browsers/{browserId}/playbooks)"
                    },
                    "failed": {
                      "type": "boolean",
                      "description": "Whether the agent reports it could not do the task (its reply starts with FAILED:, or it ran out of steps)"
                    },
                    "data": {
                      "description": "The answer in the shape of the request's schema, when one was given and the agent returned it"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "Browser not connected",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "500": {
            "description": "Chat error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/live/{browserId}": {
      "get": {
        "summary": "Live view stream",
        "description": "Server-Sent Events stream of JPEG frames from the browser. Authorization header, or a single-use ?ticket= from POST /api/control/sessions/{id}/ticket for EventSource, which cannot set headers.",
        "operationId": "liveView",
        "parameters": [
          { "$ref": "#/components/parameters/browserId" },
          {
            "name": "key",
            "in": "query",
            "schema": { "type": "string" },
            "description": "API key (alternative to Authorization header, for EventSource)"
          }
        ],
        "responses": {
          "200": {
            "description": "SSE stream of base64 JPEG frames",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Each event contains a data: line with a base64-encoded JPEG data URL"
                }
              }
            }
          },
          "404": {
            "description": "Browser not connected",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/mcp/{browserId}": {
      "post": {
        "summary": "MCP Streamable HTTP endpoint",
        "description": "Model Context Protocol endpoint for AI tool integration (Claude Desktop, Cursor, etc.). Implements the MCP Streamable HTTP transport.",
        "operationId": "mcpPost",
        "parameters": [
          { "$ref": "#/components/parameters/browserId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "MCP JSON-RPC request"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MCP JSON-RPC response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "MCP JSON-RPC response"
                }
              }
            }
          },
          "404": {
            "description": "Browser not connected",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      },
      "get": {
        "summary": "MCP SSE endpoint",
        "operationId": "mcpGet",
        "parameters": [
          { "$ref": "#/components/parameters/browserId" }
        ],
        "responses": {
          "200": {
            "description": "MCP SSE stream",
            "content": {
              "text/event-stream": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "MCP session cleanup",
        "operationId": "mcpDelete",
        "parameters": [
          { "$ref": "#/components/parameters/browserId" }
        ],
        "responses": {
          "200": {
            "description": "Session closed"
          }
        }
      }
    },
    "/pool": {
      "get": {
        "summary": "Pool status",
        "operationId": "getPool",
        "description": "Returns pool size and list of connected browsers sharing the caller's API key.",
        "responses": {
          "200": {
            "description": "Pool status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "size": { "type": "integer", "description": "Number of browsers in pool" },
                    "browsers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "name": { "type": "string" },
                          "currentUrl": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/pool/command": {
      "post": {
        "summary": "Round-robin command",
        "operationId": "poolCommand",
        "description": "Send a command to the next browser in the pool via round-robin. Response includes _browser field showing which browser handled it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["action"],
                "properties": {
                  "action": { "type": "string", "description": "Command action (navigate, analyze, click, type, etc.)" },
                  "params": { "type": "object", "description": "Action parameters" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Command result",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/CommandResult" },
                    {
                      "type": "object",
                      "properties": {
                        "_browser": { "type": "string", "description": "Browser ID that handled the command" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "No browsers available in pool",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/pool/cookies": {
      "get": {
        "summary": "Export a persona's cookie jar",
        "operationId": "getPoolCookies",
        "description": "The cookies of one of the caller's personas: the logins its browsers share. `format=playwright` answers the shape `context.addCookies()` takes; `format=netscape` answers a cookies.txt download for curl, wget and yt-dlp.",
        "parameters": [
          { "name": "persona", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Persona id; the key's default persona when omitted." },
          { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["json", "playwright", "netscape"], "default": "json" } }
        ],
        "responses": {
          "200": {
            "description": "Cookie jar",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cookies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "value": { "type": "string" },
                          "domain": { "type": "string" },
                          "path": { "type": "string" },
                          "secure": { "type": "boolean" },
                          "httpOnly": { "type": "boolean" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Import cookies into a persona's jar",
        "operationId": "importPoolCookies",
        "description": "Merges cookies into one of the caller's persona jars. The persona's browsers pick them up on their next visit to each site. Use it to bring in logins exported from another browser, or to copy one persona's logins to another. Audited.",
        "parameters": [
          { "name": "persona", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Persona id; the key's default persona when omitted." }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["cookies"],
                "properties": {
                  "cookies": {
                    "type": "array",
                    "maxItems": 20000,
                    "items": {
                      "type": "object",
                      "required": ["name", "value", "domain"],
                      "properties": {
                        "name": { "type": "string" },
                        "value": { "type": "string" },
                        "domain": { "type": "string" },
                        "path": { "type": "string" },
                        "secure": { "type": "boolean" },
                        "httpOnly": { "type": "boolean" },
                        "sameSite": { "type": "string" },
                        "expirationDate": { "type": "number", "description": "Seconds since the epoch; `expires` is accepted too. Omit for a session cookie." }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was merged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "persona": { "type": "string" },
                    "imported": { "type": "integer" },
                    "skipped": { "type": "integer", "description": "Cookies without a name, value or domain, or already expired." },
                    "total": { "type": "integer", "description": "Cookies in the jar afterwards." }
                  }
                }
              }
            }
          },
          "400": { "description": "No cookie list, or more than 20000 cookies" }
        }
      },
      "delete": {
        "summary": "Clear shared cookie jar",
        "operationId": "clearPoolCookies",
        "responses": {
          "200": {
            "description": "Cookies cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/fleet/provision": {
      "post": {
        "summary": "Batch-generate API keys",
        "operationId": "fleetProvision",
        "description": "Generate N API keys in one call. Admin key required. Use query param or body to specify count (max 10000).",
        "parameters": [
          {
            "name": "count",
            "in": "query",
            "schema": { "type": "integer", "default": 1 },
            "description": "Number of keys to generate"
          }
        ],
        "responses": {
          "200": {
            "description": "Generated keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "count": { "type": "integer" },
                    "keys": {
                      "type": "array",
                      "items": { "type": "string" }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Admin key required",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/mcp/pool": {
      "post": {
        "summary": "Pool MCP endpoint",
        "operationId": "mcpPool",
        "description": "MCP Streamable HTTP endpoint for the browser pool. Round-robins commands across all browsers sharing the caller's API key. navigate/analyze_page advance the round-robin; click/type/screenshot stay pinned to the last-used browser.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "MCP JSON-RPC request"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MCP JSON-RPC response or SSE stream",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "MCP JSON-RPC response"
                }
              }
            }
          }
        }
      }
    },
    "/config": {
      "get": {
        "summary": "Get this key's settings",
        "description": "The calling key's settings, secrets masked, with the LLM it runs on (effective) and the provider and model catalog.",
        "operationId": "getConfig",
        "responses": {
          "200": {
            "description": "The key's settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "llm_provider": {
                      "type": "string",
                      "enum": [
                        "openai",
                        "anthropic",
                        "gemini",
                        "vertex",
                        "openrouter"
                      ],
                      "description": "Which LLM provider this key's agent runs on; picks the default endpoint and model."
                    },
                    "openai_api_key": {
                      "type": "string",
                      "description": "The key for whichever llm_provider is set (the field name is shared). Masked on read."
                    },
                    "openai_base_url": {
                      "type": "string",
                      "description": "An OpenAI-compatible endpoint, when not the provider's own. Only honoured with this key's own openai_api_key."
                    },
                    "chat_model": {
                      "type": "string",
                      "description": "The model id, as the provider names it. Empty or null uses the provider's default."
                    },
                    "effective": {
                      "type": "object",
                      "description": "What this key runs on right now, deployment defaults included.",
                      "properties": {
                        "baseUrl": {
                          "type": "string"
                        },
                        "model": {
                          "type": "string"
                        },
                        "hasLlmKey": {
                          "type": "boolean"
                        }
                      }
                    },
                    "llm_catalog": {
                      "type": "array",
                      "description": "Every LLM provider and the models it offers (OpenRouter's is its live list). Clients build their provider and model pickers from this.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "hint": {
                            "type": "string",
                            "description": "How its key looks."
                          },
                          "keysUrl": {
                            "type": "string"
                          },
                          "base": {
                            "type": "string"
                          },
                          "model": {
                            "type": "string",
                            "description": "The default model."
                          },
                          "models": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string"
                                },
                                "label": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Update this key's settings",
        "description": "Sets only the fields sent; null or empty clears one. Send llm_provider and chat_model together. Connected desktop apps on the key are told to re-read their model.",
        "operationId": "setConfig",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "llm_provider": {
                    "type": "string",
                    "enum": [
                      "openai",
                      "anthropic",
                      "gemini",
                      "vertex",
                      "openrouter"
                    ],
                    "description": "Which LLM provider this key's agent runs on; picks the default endpoint and model."
                  },
                  "openai_api_key": {
                    "type": "string",
                    "description": "The key for whichever llm_provider is set (the field name is shared). Masked on read."
                  },
                  "openai_base_url": {
                    "type": "string",
                    "description": "An OpenAI-compatible endpoint, when not the provider's own. Only honoured with this key's own openai_api_key."
                  },
                  "chat_model": {
                    "type": "string",
                    "description": "The model id, as the provider names it. Empty or null uses the provider's default."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; the body is the new settings, as GET returns them, with ok: true",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/config/site-notes": {
      "get": {
        "summary": "What the agent remembered about each site",
        "description": "Notes the agent chose to keep for this key while working a site (where a report lives, a form quirk). It writes them from what it read on a page, so they are readable here and can be deleted.",
        "operationId": "listSiteNotes",
        "responses": {
          "200": {
            "description": "Notes by host",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notes": {
                      "type": "object",
                      "additionalProperties": { "type": "array", "items": { "type": "string" } }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/config/site-notes/{host}": {
      "delete": {
        "summary": "Forget the notes kept for one site",
        "operationId": "forgetSiteNotes",
        "parameters": [
          {
            "name": "host",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The site's host, as GET /config/site-notes lists it"
          }
        ],
        "responses": {
          "200": {
            "description": "The notes that remain",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "notes": { "type": "object" }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key passed as Bearer token. Create one from the dashboard after signing in."
      }
    },
    "parameters": {
      "browserId": {
        "name": "browserId",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Browser session ID (from GET /browsers)"
      }
    },
    "schemas": {
      "ControlSession": {
        "type": "object",
        "required": [
          "id",
          "project",
          "state",
          "control"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "project": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "queued",
              "provisioning",
              "ready",
              "disconnected",
              "cleanup_pending",
              "unknown_outcome",
              "stopped",
              "failed"
            ]
          },
          "managed": {
            "type": "boolean"
          },
          "costUsd": {
            "type": "number"
          },
          "control": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "agent",
                  "human",
                  "paused"
                ]
              },
              "expiresAt": {
                "type": "integer",
                "nullable": true
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string", "description": "What happened, why, and the next step. Stands alone: clients show it as is." },
          "code": { "type": "string", "description": "A stable reason to branch on: invalid_request, not_found, internal_error, command_outcome_unknown, action_unknown, action_unsupported, action_forbidden, tab_unprotected, endpoint_in_use, provider_failed, quota_exceeded, llm_unconfigured, and others named per route." },
          "field": { "type": "string", "description": "For invalid_request: the field that was wrong." },
          "ref": { "type": "string", "description": "For internal_error: eight hex characters that also sit in the server's log line for this failure." },
          "browserId": { "type": "string", "description": "For endpoint_in_use: the browser that holds the endpoint." },
          "action": { "type": "string", "description": "For action_unknown, action_unsupported and action_forbidden: the action as sent." },
          "clientType": { "type": "string", "description": "For action_unsupported: this browser's kind, oya or cdp." },
          "supportedOn": {
            "type": "array",
            "items": { "type": "string" },
            "description": "For action_unsupported: the kinds of browser that do the action."
          }
        }
      },
      "Browser": {
        "type": "object",
        "properties": {
          "actions": {
            "type": "array",
            "items": { "type": "string" },
            "description": "On one browser's detail: the actions it does, sorted, one spelling each."
          },
          "id": { "type": "string", "description": "Browser session ID" },
          "name": { "type": "string", "description": "Browser name (e.g. 'Chrome on macOS')" },
          "currentUrl": { "type": "string", "description": "Current page URL" },
          "connectedAt": { "type": "string", "format": "date-time" },
          "lastSeenAt": { "type": "string", "format": "date-time" }
        }
      },
      "CommandResult": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "description": "Whether the command succeeded" },
          "code": { "type": "string", "description": "For ok:false, a stable reason: command_failed for a refusal the page answered, or one named per action." },
          "data": {
            "type": "object",
            "description": "Action-specific result data",
            "properties": {
              "url": { "type": "string" },
              "title": { "type": "string" },
              "screenshot": { "type": "string", "description": "Base64 PNG data URL (for screenshot action)" },
              "format": { "type": "string", "enum": ["markdown", "toon", "jsonl"], "description": "The format page is written in (for analyze action)" },
              "page": { "type": "string", "description": "The page, written in format (for analyze action)" },
              "markdown": { "type": "string", "description": "Page content as markdown, when format is markdown (for analyze action)" },
              "blocks": { "type": "array", "description": "The page as data: one block per heading, paragraph, list item, table row, image and element, in reading order (for analyze action)", "items": { "type": "object" } },
              "elements": {
                "type": "array",
                "description": "Interactive elements (for analyze/read_page actions)",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "integer" },
                    "type": { "type": "string" },
                    "text": { "type": "string" },
                    "href": { "type": "string" },
                    "visible": { "type": "boolean" }
                  }
                }
              },
              "tabs": {
                "type": "array",
                "description": "Open tabs (for list_tabs action)",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "integer" },
                    "title": { "type": "string" },
                    "url": { "type": "string" },
                    "active": { "type": "boolean" }
                  }
                }
              },
              "tab_id": { "type": "integer", "description": "New tab ID (for open_tab action)" },
              "selector": { "type": "string" },
              "direction": { "type": "string" },
              "amount": { "type": "number" },
              "key": { "type": "string" },
              "found": { "type": "boolean" },
              "closed": { "type": "boolean" },
              "needs_capture": { "type": "boolean" },
              "scroll": {
                "type": "object",
                "properties": {
                  "top": { "type": "number" },
                  "height": { "type": "number" },
                  "scrollHeight": { "type": "number" }
                }
              },
              "viewport": {
                "type": "object",
                "properties": {
                  "width": { "type": "number" },
                  "height": { "type": "number" }
                }
              },
              "truncated": { "type": "boolean" }
            }
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Error message if ok is false"
          }
        }
      }
    }
  }
}
