{
  "openapi": "3.1.0",
  "info": {
    "title": "Landtest API",
    "version": "1.0.0",
    "summary": "Landtest REST API v1, MCP server, event ingest and OAuth endpoints.",
    "description": "Landtest is an AI agent that A/B tests landing pages. The versioned REST API lives under /api/v1 (projects, tests, stats, async variant jobs, batch). Authenticate with OAuth 2.1 or a personal access token; try it without an account using the sandbox key `lt_sandbox_demo`. Writes accept an Idempotency-Key header, every response carries RateLimit headers, list endpoints use cursor pagination, and errors are JSON with a stable code and a hint. The same operations are available as MCP tools at /mcp.",
    "contact": {
      "email": "hello@zerocost.app",
      "url": "https://landtest.dev/contact"
    },
    "x-api-versioning": {
      "strategy": "url-path",
      "current": "v1",
      "policy": "https://landtest.dev/developers#versioning",
      "deprecation": "Deprecated surfaces keep working for at least 6 months and send Deprecation (RFC 9745), Sunset (RFC 8594) and Link rel=successor-version headers."
    },
    "x-sandbox": {
      "description": "Use 'Authorization: Bearer lt_sandbox_demo' against the same base URL for a fixed demo workspace. Writes are validated and simulated; nothing persists.",
      "key": "lt_sandbox_demo"
    }
  },
  "servers": [
    {
      "url": "https://landtest.dev",
      "description": "Production (use the sandbox key for test mode)"
    }
  ],
  "externalDocs": {
    "description": "Developer portal",
    "url": "https://landtest.dev/developers"
  },
  "tags": [
    {
      "name": "REST v1",
      "description": "Versioned REST API. Breaking changes only ship as /api/v2."
    },
    {
      "name": "MCP",
      "description": "Model Context Protocol server."
    },
    {
      "name": "Analytics",
      "description": "Event ingest used by the Landtest tracker on customer sites."
    },
    {
      "name": "OAuth",
      "description": "OAuth 2.1 with PKCE and dynamic client registration."
    },
    {
      "name": "Waitlist"
    }
  ],
  "paths": {
    "/api/v1/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "REST v1"
        ],
        "summary": "Liveness check (no auth)",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/status": {
      "get": {
        "operationId": "getStatus",
        "tags": [
          "REST v1"
        ],
        "summary": "Component status (no auth)",
        "security": [],
        "responses": {
          "200": {
            "description": "Component status.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/keys": {
      "post": {
        "operationId": "createApiKey",
        "tags": [
          "REST v1"
        ],
        "summary": "Create a self-serve sandbox API key (no auth, no signup)",
        "description": "Returns a signed sandbox key valid for 7 days, scoped to the demo workspace. Production keys are self-serve too: sign up free and create one under Settings -> MCP, or use OAuth.",
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          },
          "500": {
            "$ref": "#/components/responses/E500"
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "REST v1"
        ],
        "summary": "The workspace, scopes and AI credit balance of the calling token",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Caller identity.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "REST v1"
        ],
        "summary": "List projects (connected repos), cursor-paginated",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of projects.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/tests": {
      "get": {
        "operationId": "listTests",
        "tags": [
          "REST v1"
        ],
        "summary": "List tests in a project, cursor-paginated",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Project UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "enum": [
                "draft",
                "running",
                "paused",
                "archived"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of tests.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TestSummary"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      },
      "post": {
        "operationId": "createTest",
        "tags": [
          "REST v1"
        ],
        "summary": "Create a draft test (nothing is written to GitHub)",
        "security": [
          {
            "oauth2": [
              "write"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Project UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "page_path"
                ],
                "properties": {
                  "page_path": {
                    "type": "string",
                    "pattern": "^/",
                    "examples": [
                      "/",
                      "/pricing"
                    ]
                  },
                  "label": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "goal_type": {
                    "enum": [
                      "url",
                      "click"
                    ]
                  },
                  "goal_value": {
                    "type": "string",
                    "description": "URL path (\"/signup\") or CSS selector (\"#cta\")."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft test created.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}": {
      "get": {
        "operationId": "getTest",
        "tags": [
          "REST v1"
        ],
        "summary": "A test's configuration, weights and variants",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Test detail.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TestDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}/stats": {
      "get": {
        "operationId": "getTestStats",
        "tags": [
          "REST v1"
        ],
        "summary": "Per-variant conversion rate, lift, mSPRT confidence, SRM and a verdict",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Test statistics.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TestStats"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}/variants": {
      "post": {
        "operationId": "createVariantJob",
        "tags": [
          "REST v1"
        ],
        "summary": "Start AI variant generation (async: 202 + job)",
        "description": "Generation takes 30-120s, so it runs as a job. The response is 202 Accepted with a job id, a Location header pointing at GET /api/v1/jobs/{jobId}, and Retry-After. Poll until status is succeeded or failed. Commits to the preview branch only and uses AI credits.",
        "security": [
          {
            "oauth2": [
              "write"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt"
                ],
                "properties": {
                  "prompt": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 4000
                  },
                  "mode": {
                    "enum": [
                      "new",
                      "iterate"
                    ]
                  },
                  "iterate_slot": {
                    "type": "string",
                    "examples": [
                      "variant-1"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the job.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "Location": {
                "description": "URL of the job to poll.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before polling.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "402": {
            "$ref": "#/components/responses/E402"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/jobs/{jobId}": {
      "get": {
        "operationId": "getJob",
        "tags": [
          "REST v1"
        ],
        "summary": "Poll an async job",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job state. While queued or running, Retry-After says when to poll again.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}/weights": {
      "put": {
        "operationId": "updateWeights",
        "tags": [
          "REST v1"
        ],
        "summary": "Set traffic weights (must sum to 100)",
        "security": [
          {
            "oauth2": [
              "write"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "weights"
                ],
                "properties": {
                  "weights": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100
                    },
                    "examples": [
                      {
                        "control": 50,
                        "variant-1": 50
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Weights updated.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}/goal": {
      "put": {
        "operationId": "updateGoal",
        "tags": [
          "REST v1"
        ],
        "summary": "Set the conversion goal",
        "security": [
          {
            "oauth2": [
              "write"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "value"
                ],
                "properties": {
                  "type": {
                    "enum": [
                      "url",
                      "click"
                    ]
                  },
                  "value": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Goal updated.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}/pause": {
      "post": {
        "operationId": "pauseTest",
        "tags": [
          "REST v1"
        ],
        "summary": "Pause a running test",
        "security": [
          {
            "oauth2": [
              "write"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Paused.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/tests/{test}/resume": {
      "post": {
        "operationId": "resumeTest",
        "tags": [
          "REST v1"
        ],
        "summary": "Resume a paused test",
        "security": [
          {
            "oauth2": [
              "write"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "name": "test",
            "in": "path",
            "required": true,
            "description": "Test ULID (01J...) or UUID.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Resumed.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "402": {
            "$ref": "#/components/responses/E402"
          },
          "403": {
            "$ref": "#/components/responses/E403"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "422": {
            "$ref": "#/components/responses/E422"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/v1/batch": {
      "post": {
        "operationId": "runBatch",
        "tags": [
          "REST v1"
        ],
        "summary": "Run up to 20 operations in one request",
        "description": "Each operation is {id?, method, path, body?} with a path relative to /api/v1 (e.g. GET /tests/{test}/stats). Operations run in order; one failing does not stop the rest. Each result carries its own status and body.",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "operations"
                ],
                "properties": {
                  "operations": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "method",
                        "path"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "method": {
                          "enum": [
                            "GET",
                            "POST",
                            "PUT",
                            "PATCH"
                          ]
                        },
                        "path": {
                          "type": "string",
                          "examples": [
                            "/tests/01J.../stats"
                          ]
                        },
                        "body": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-operation results.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "401": {
            "$ref": "#/components/responses/E401"
          },
          "429": {
            "$ref": "#/components/responses/E429"
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "operationId": "callMcpServerLegacy",
        "tags": [
          "MCP"
        ],
        "deprecated": true,
        "summary": "Legacy alias of POST /mcp (deprecated, sunset 2027-04-30)",
        "description": "Responses carry Deprecation, Sunset and Link rel=successor-version headers. Use /mcp.",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Error 401",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error 429",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "callMcpServer",
        "tags": [
          "MCP"
        ],
        "summary": "Send a JSON-RPC message to the Landtest MCP server",
        "description": "Streamable HTTP transport (stateless, JSON responses). Exposes 22 tools; see https://landtest.dev/developers. An unauthenticated call returns 401 with a WWW-Authenticate header pointing at the OAuth protected-resource metadata.",
        "security": [
          {
            "oauth2": [
              "read"
            ]
          },
          {
            "bearerToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "properties": {
                  "jsonrpc": {
                    "const": "2.0"
                  },
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "method": {
                    "type": "string",
                    "examples": [
                      "tools/list",
                      "tools/call"
                    ]
                  },
                  "params": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Error 401",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error 429",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/ingest": {
      "post": {
        "operationId": "ingestEvents",
        "tags": [
          "Analytics"
        ],
        "summary": "Send tracker events for a running test",
        "description": "Used by the Landtest tracker (lt.js) on customer sites. Always answers 200 with a small JSON body, even when events are dropped, so analytics can never break a customer site. Authenticated with the project's write key.",
        "security": [
          {
            "writeKey": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "X-LT-Write-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/IngestBatch"
                  },
                  {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/IngestEvent"
                    }
                  },
                  {
                    "$ref": "#/components/schemas/IngestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (or silently dropped).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "accepted": {
                      "type": "integer"
                    },
                    "limited": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/waitlist": {
      "post": {
        "operationId": "joinWaitlist",
        "tags": [
          "Waitlist"
        ],
        "summary": "Join the Landtest waitlist",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "name",
                  "website",
                  "workspaceName"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "website": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048
                  },
                  "workspaceName": {
                    "type": "string",
                    "maxLength": 49,
                    "pattern": "^[a-zA-Z0-9][a-zA-Z0-9 _-]{1,48}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error 400",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error 500",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error 503",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/register": {
      "post": {
        "operationId": "registerOAuthClient",
        "tags": [
          "OAuth"
        ],
        "summary": "Dynamic client registration (RFC 7591)",
        "description": "Registers a public PKCE client. Rate limited to 30 per hour per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "redirect_uris"
                ],
                "properties": {
                  "client_name": {
                    "type": "string"
                  },
                  "redirect_uris": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "token_endpoint_auth_method": {
                    "enum": [
                      "none",
                      "client_secret_post",
                      "client_secret_basic"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client registered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthClient"
                }
              }
            }
          },
          "400": {
            "description": "invalid_client_metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "operationId": "exchangeOAuthToken",
        "tags": [
          "OAuth"
        ],
        "summary": "Exchange an authorization code or refresh token",
        "description": "Grants: authorization_code (with PKCE S256 code_verifier) and refresh_token. Access tokens last 1 hour; refresh tokens rotate and last 60 days.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "enum": [
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "code": {
                    "type": "string"
                  },
                  "code_verifier": {
                    "type": "string"
                  },
                  "redirect_uri": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "refresh_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthToken"
                }
              }
            }
          },
          "400": {
            "description": "invalid_grant or invalid_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/revoke": {
      "post": {
        "operationId": "revokeOAuthToken",
        "tags": [
          "OAuth"
        ],
        "summary": "Revoke a token (RFC 7009)",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked (also returned for unknown tokens).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 authorization code with PKCE. Scopes: read, write.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://landtest.dev/oauth/authorize",
            "tokenUrl": "https://landtest.dev/oauth/token",
            "refreshUrl": "https://landtest.dev/oauth/token",
            "scopes": {
              "read": "Read tests, stats and memory",
              "write": "Create, start, pause and ship tests"
            }
          }
        }
      },
      "bearerToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Personal access token (lt_mcp_...) created in Settings -> MCP."
      },
      "writeKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-LT-Write-Key",
        "description": "Per-project write key for event ingest."
      }
    },
    "responses": {
      "E400": {
        "description": "Invalid request (bad JSON, missing field, bad cursor).",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E401": {
        "description": "Missing or invalid token. WWW-Authenticate points at the OAuth metadata.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E402": {
        "description": "Plan or AI credit limit reached.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E403": {
        "description": "Token lacks the write scope.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E404": {
        "description": "Resource not found.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E405": {
        "description": "Method not allowed. See the Allow header.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E409": {
        "description": "Conflict (integration missing, or the same Idempotency-Key is still in flight).",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E422": {
        "description": "Request understood but not processable (or Idempotency-Key reused for a different request).",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E429": {
        "description": "Rate limited. Wait Retry-After seconds.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "E500": {
        "description": "Server error. Retry with backoff.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Unique key (e.g. a UUID v4) that makes a write safe to retry. The first response is stored for 24 hours and replayed for retries with the same key and body (Idempotent-Replayed: true). Reusing a key with a different body returns 422.",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "next_cursor from the previous page.",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size (1-100, default 25).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Requests allowed in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitPolicy": {
        "description": "Quota policy, e.g. \"120;w=60\".",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "docs"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code, e.g. not_found, rate_limited, idempotency_key_reused."
              },
              "message": {
                "type": "string"
              },
              "hint": {
                "type": "string",
                "description": "What to do to resolve it."
              },
              "docs": {
                "type": "string",
                "format": "uri"
              },
              "request_id": {
                "type": "string"
              }
            }
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": [
          "key",
          "type",
          "expires_at"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Bearer token (lt_sbx_...)."
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "const": "sandbox"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "base_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": true
      },
      "Pagination": {
        "type": "object",
        "required": [
          "limit",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "limit": {
            "type": "integer"
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as ?cursor= to get the next page. Opaque."
          }
        },
        "additionalProperties": true
      },
      "Health": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "Status": {
        "type": "object",
        "required": [
          "status",
          "components"
        ],
        "properties": {
          "status": {
            "enum": [
              "operational",
              "degraded"
            ]
          },
          "version": {
            "type": "string"
          },
          "components": {
            "type": "object",
            "additionalProperties": {
              "enum": [
                "operational",
                "degraded"
              ]
            }
          },
          "sandbox": {
            "type": "object",
            "properties": {
              "available": {
                "type": "boolean"
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            },
            "additionalProperties": true
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "Me": {
        "type": "object",
        "properties": {
          "workspace": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "token": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "enum": [
                    "read",
                    "write"
                  ]
                }
              }
            },
            "additionalProperties": true
          },
          "credits": {
            "type": [
              "object",
              "null"
            ]
          },
          "sandbox": {
            "type": "boolean"
          }
        },
        "additionalProperties": true
      },
      "Project": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "repo": {
            "type": "string"
          },
          "publish_mode": {
            "enum": [
              "auto",
              "review",
              "manual"
            ]
          },
          "tests": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "running": {
                "type": "integer"
              },
              "draft": {
                "type": "integer"
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "TestSummary": {
        "type": "object",
        "required": [
          "ulid",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "ulid": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "page_path": {
            "type": "string"
          },
          "status": {
            "enum": [
              "draft",
              "running",
              "paused",
              "archived"
            ]
          },
          "visitors": {
            "type": "integer"
          },
          "conversions": {
            "type": "integer"
          },
          "conversion_rate": {
            "type": "number"
          },
          "studio_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": true
      },
      "TestDetail": {
        "type": "object",
        "required": [
          "test"
        ],
        "properties": {
          "project": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "test": {
            "$ref": "#/components/schemas/TestSummary"
          },
          "variants": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "additionalProperties": true
      },
      "TestStats": {
        "type": "object",
        "properties": {
          "test": {
            "type": "object"
          },
          "variants": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slot": {
                  "type": "string"
                },
                "visitors": {
                  "type": "integer"
                },
                "conversions": {
                  "type": "integer"
                },
                "conversion_rate": {
                  "type": "number"
                },
                "lift_vs_control": {
                  "type": "number"
                },
                "confidence": {
                  "type": "number"
                }
              },
              "additionalProperties": true
            }
          },
          "srm": {
            "type": "object",
            "properties": {
              "alert": {
                "type": "boolean"
              }
            },
            "additionalProperties": true
          },
          "verdict": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "WriteResult": {
        "type": "object",
        "properties": {
          "sandbox": {
            "type": "boolean"
          },
          "test": {
            "type": "object"
          },
          "mode": {
            "type": "string"
          },
          "next": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "status",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "const": "variant_generation"
          },
          "status": {
            "enum": [
              "queued",
              "running",
              "succeeded",
              "failed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "result": {
            "type": [
              "object",
              "null"
            ]
          },
          "error": {
            "type": [
              "object",
              "null"
            ]
          }
        },
        "additionalProperties": true
      },
      "JobAccepted": {
        "type": "object",
        "required": [
          "job",
          "poll"
        ],
        "properties": {
          "job": {
            "$ref": "#/components/schemas/Job"
          },
          "poll": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "interval_seconds": {
                "type": "integer"
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "JobEnvelope": {
        "type": "object",
        "required": [
          "job"
        ],
        "properties": {
          "job": {
            "$ref": "#/components/schemas/Job"
          }
        },
        "additionalProperties": true
      },
      "BatchResult": {
        "type": "object",
        "required": [
          "results",
          "summary"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "status",
                "body"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "status": {
                  "type": "integer"
                },
                "body": {}
              },
              "additionalProperties": true
            }
          },
          "summary": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "succeeded": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "JSON error body. `error` is a message string or, on newer endpoints, an object with a stable code.",
        "properties": {
          "error": {
            "oneOf": [
              {
                "type": "string",
                "description": "Human-readable message."
              },
              {
                "type": "object",
                "required": [
                  "code",
                  "message"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "Stable machine-readable error code."
                  },
                  "message": {
                    "type": "string",
                    "description": "Human-readable explanation."
                  },
                  "docs": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where to read how to resolve it."
                  }
                }
              }
            ]
          }
        }
      },
      "OAuthClient": {
        "type": "object",
        "required": [
          "client_id"
        ],
        "properties": {
          "client_id": {
            "type": "string"
          },
          "client_name": {
            "type": "string"
          },
          "redirect_uris": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "OAuthToken": {
        "type": "object",
        "required": [
          "access_token",
          "token_type"
        ],
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "const": "Bearer"
          },
          "expires_in": {
            "type": "integer"
          },
          "refresh_token": {
            "type": "string"
          },
          "scope": {
            "type": "string"
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "IngestEvent": {
        "type": "object",
        "required": [
          "event",
          "test_id",
          "variant_id",
          "visitor_id"
        ],
        "properties": {
          "event": {
            "enum": [
              "pageview",
              "scroll_depth",
              "section_view",
              "cta_click",
              "element_click",
              "form_submit",
              "goal_reached",
              "time_on_page"
            ]
          },
          "test_id": {
            "type": "string",
            "maxLength": 64
          },
          "variant_id": {
            "type": "string",
            "maxLength": 64
          },
          "visitor_id": {
            "type": "string",
            "maxLength": 128
          },
          "project_id": {
            "type": "string"
          },
          "session_id": {
            "type": "string",
            "maxLength": 128
          },
          "page": {
            "type": "string",
            "maxLength": 500
          },
          "referrer": {
            "type": "string",
            "maxLength": 500
          },
          "utm_source": {
            "type": "string",
            "maxLength": 100
          },
          "utm_medium": {
            "type": "string",
            "maxLength": 100
          },
          "utm_campaign": {
            "type": "string",
            "maxLength": 100
          },
          "device": {
            "type": "string",
            "maxLength": 20
          },
          "write_key": {
            "type": "string"
          },
          "payload": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "IngestBatch": {
        "type": "object",
        "required": [
          "events"
        ],
        "properties": {
          "write_key": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/IngestEvent"
            }
          }
        }
      }
    }
  }
}