{"openapi":"3.1.0","info":{"title":"Loha API","version":"v1","description":"Public REST API for the Loha CI/CD platform.\n\nAuthenticate with a personal access token (PAT) bound to a single workspace.\nAvailable scopes: builds:read, security:read, security:rescan, builds:write, repos:read, repos:write, connections:read, connections:write, pipelines:read, triggers:read, triggers:write, triggers:run, webhooks:read, webhooks:write, notifications:read, notifications:write."},"servers":[{"url":"http://app.loha.longtumlab.com"}],"components":{"securitySchemes":{"bearerToken":{"type":"http","scheme":"bearer","description":"Loha personal access token. Send as `Authorization: Bearer loha_…` or the `x-api-key` header."}},"schemas":{"WhoamiResponse":{"type":"object","properties":{"principal":{"type":"object","properties":{"type":{"type":"string","example":"user"},"id":{"type":"string","example":"usr_5e6f7a8b"},"name":{"type":["string","null"],"example":"ci-deploy-bot"},"projectId":{"type":["string","null"],"example":"proj_1a2b3c4d"}},"required":["type","id","name","projectId"]},"workspace":{"type":"object","properties":{"id":{"type":"string","example":"org_3c4d5e6f"},"slug":{"type":["string","null"],"example":"acme"},"name":{"type":["string","null"],"example":"Acme Inc."}},"required":["id","slug","name"]},"scopes":{"type":"array","items":{"type":"string"},"example":["builds:read","builds:write"]},"token":{"type":"object","properties":{"id":{"type":"string","example":"pat_9a8b7c6d"},"name":{"type":"string","example":"CI deploy token"},"prefix":{"type":"string","example":"loha_9a8b"}},"required":["id","name","prefix"]}},"required":["principal","workspace","scopes","token"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string","example":"Build not found"},"code":{"type":"string","description":"Stable machine-readable error code.","example":"VALIDATION_ERROR"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Per-field validation messages (Zod flatten) on a 400.","example":{"limit":["Number must be less than or equal to 100"]}}},"required":["error"],"description":"Error envelope returned on 4xx/5xx."},"Build":{"type":"object","properties":{"id":{"type":"string","example":"bld_2a7f1c9e4b"},"buildNumber":{"type":"integer","example":42},"pipelineName":{"type":["string","null"],"example":"ci"},"repository":{"type":["object","null"],"properties":{"id":{"type":"string","example":"repo_8d31aa02"},"fullName":{"type":["string","null"],"example":"roo-cloud/loha"},"htmlUrl":{"type":["string","null"],"example":"https://git.openlandscape.cloud/roo-cloud/loha"}},"required":["id","fullName","htmlUrl"]},"status":{"type":"string","description":"PENDING | QUEUED | RUNNING | SUCCESS | FAILED | CANCELLED | TIMEOUT | SKIPPED.","example":"SUCCESS"},"branch":{"type":"string","example":"main"},"commit":{"type":"object","properties":{"sha":{"type":"string","example":"9f3c1b2d4e5a6f7081920a3b4c5d6e7f80123456"},"shortSha":{"type":"string","example":"9f3c1b2"},"message":{"type":"string","example":"feat(dashboard): add API reference"},"author":{"type":"string","example":"Aaron"}},"required":["sha","shortSha","message","author"]},"trigger":{"type":"object","properties":{"type":{"type":["string","null"],"example":"webhook"},"id":{"type":["string","null"],"example":"trg_1c2d3e4f"},"name":{"type":["string","null"],"example":"push to main"}},"required":["type","id","name"]},"duration":{"type":"integer","description":"Seconds.","example":87},"tags":{"type":"array","items":{"type":"string"},"example":["release"]},"labels":{"type":"object","additionalProperties":{"type":"string"},"example":{"env":"prod"}},"createdAt":{"type":"string","example":"2026-05-26T09:14:02.000Z"},"startedAt":{"type":"string","example":"2026-05-26T09:14:05.000Z"},"finishedAt":{"type":"string","example":"2026-05-26T09:15:32.000Z"}},"required":["id","buildNumber","pipelineName","repository","status","branch","commit","trigger","tags","labels","createdAt"],"description":"A pipeline build (run) in the workspace."},"Pagination":{"type":"object","properties":{"total":{"type":"integer","example":134},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0},"hasMore":{"type":"boolean","example":true}},"required":["total","limit","offset","hasMore"],"description":"Offset-based pagination envelope."},"BuildListResponse":{"type":"object","properties":{"builds":{"type":"array","items":{"$ref":"#/components/schemas/Build"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["builds","pagination"]},"BuildDetailResponse":{"type":"object","properties":{"build":{"$ref":"#/components/schemas/Build"}},"required":["build"]},"CancelBuildResponse":{"type":"object","properties":{"message":{"type":"string","example":"Build cancelled"},"accepted":{"type":"boolean","example":true},"build":{"type":"object","properties":{"id":{"type":"string","example":"bld_2a7f1c9e4b"},"buildNumber":{"type":"integer","example":42},"status":{"type":"string","example":"cancelled"}},"required":["id","buildNumber","status"]}},"required":["message","build"]},"BuildLogsResponse":{"type":"object","properties":{"logs":{"type":"array","items":{"type":"object","properties":{"line":{"type":"integer","example":1},"timestamp":{"type":"string","example":"2026-05-26T09:14:05.123Z"},"level":{"type":"string","description":"info | warn | error.","example":"info"},"stepName":{"type":["string","null"],"example":"build"},"content":{"type":"string","description":"Secret-sanitized log line.","example":"[09:14:05] Cloning repository..."}},"required":["line","timestamp","level","stepName","content"]}},"source":{"type":"string","description":"Present and 'archive' when logs were served from cold storage.","example":"archive"},"archivedAt":{"type":"string","example":"2026-05-20T00:00:00.000Z"},"status":{"type":"string","description":"Build status; present when no log lines are available.","example":"SUCCESS"},"unavailable":{"type":"boolean","description":"True when a terminal build has no retained logs.","example":false},"truncated":{"type":"boolean","description":"True when more log lines remain past this page — fetch the next offset.","example":false},"offset":{"type":"integer","description":"Offset of the first returned line.","example":0},"limit":{"type":"integer","description":"Page size applied to this response.","example":1000}},"required":["logs","truncated","offset","limit"]},"Release":{"type":"object","properties":{"id":{"type":"string","example":"rel_6b204f1c"},"serviceId":{"type":"string","example":"svc_9e33ab10"},"serviceName":{"type":"string","description":"The image's basename — display only. A repository publishing two images has two services.","example":"api"},"repositoryId":{"type":"string","example":"repo_8d31aa02"},"repositoryName":{"type":"string","example":"loha"},"commitSha":{"type":"string","example":"9f2c1b7e5a4d3c2b1a09f8e7d6c5b4a39281706f"},"imageUrl":{"type":"string","description":"Bare image repository reference — no tag, no digest.","example":"harbor.example.com/acme/api"},"imageDigest":{"type":"string","description":"The manifest digest. Join with imageUrl using `@` for a deployable reference.","example":"sha256:9f2c1b7e5a4d3c2b1a09f8e7d6c5b4a392817065f4e3d2c1b0a9988776655443"},"sourceRunId":{"type":["string","null"],"description":"The build that produced this. NULL once that build is swept by retention — a release outlives its build, and only the pointer clears.","example":"bld_2a7f1c9e4b"},"createdAt":{"type":"string","example":"2026-08-16T04:12:30.000Z"}},"required":["id","serviceId","serviceName","repositoryId","repositoryName","commitSha","imageUrl","imageDigest","sourceRunId","createdAt"],"description":"A record that one commit produced one container image, via one build."},"ReleaseListResponse":{"type":"object","properties":{"releases":{"type":"array","items":{"$ref":"#/components/schemas/Release"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["releases","pagination"]},"Repository":{"type":"object","properties":{"id":{"type":"string","example":"repo_8d31aa02"},"fullName":{"type":["string","null"],"example":"roo-cloud/loha"},"htmlUrl":{"type":["string","null"],"example":"https://git.openlandscape.cloud/roo-cloud/loha"},"provider":{"type":"string","example":"gitlab"},"defaultBranch":{"type":["string","null"],"example":"main"},"origin":{"type":"string","example":"connected"}},"required":["id","fullName","htmlUrl","provider","defaultBranch"],"description":"A connected source repository. Additional fields beyond those shown may be present."},"RepoListResponse":{"type":"object","properties":{"repositories":{"type":"array","items":{"$ref":"#/components/schemas/Repository"}},"pagination":{"type":"object","properties":{"total":{"type":"integer","example":134},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0},"hasMore":{"type":"boolean","example":true}},"required":["total","limit","offset","hasMore"],"description":"Present only when a filter/pagination query param is supplied."}},"required":["repositories"]},"RepoResponse":{"type":"object","properties":{"repository":{"$ref":"#/components/schemas/Repository"},"message":{"type":"string","example":"Repository connected. Create a trigger to set up builds."}},"required":["repository","message"]},"AddRepoBody":{"type":"object","properties":{"fullName":{"type":"string","minLength":3,"maxLength":255,"pattern":"^[\\w.-]+\\/[\\w./-]+$","example":"group/project"},"htmlUrl":{"type":"string","maxLength":500,"format":"uri","example":"https://gitlab.example.com/group/project"},"avatarUrl":{"type":["string","null"],"maxLength":1000,"format":"uri"},"defaultBranch":{"type":"string","maxLength":100,"pattern":"^[\\w./-]+$","default":"main","example":"main"},"configPath":{"type":"string","maxLength":255,"pattern":"^[\\w./-]+$","default":".loha.yml","example":".loha.yml"},"provider":{"type":"string","enum":["gitlab","github","gitea"],"default":"gitlab","example":"gitlab"},"projectId":{"type":"string","example":"proj_9a8b7c6d"},"scmConnectionId":{"type":"string","example":"scm_9a8b7c6d"},"apiUrl":{"type":"string","maxLength":500,"format":"uri","example":"https://gitlab.example.com"},"accessToken":{"type":"string","maxLength":500}},"required":["fullName","htmlUrl"]},"ScmConnection":{"type":"object","properties":{"id":{"type":"string","example":"scm_9a8b7c6d"},"name":{"type":"string","example":"Company GitLab"},"provider":{"type":"string","example":"gitlab"},"apiUrl":{"type":["string","null"],"example":"https://gitlab.example.com"},"isValid":{"type":["boolean","null"],"example":true},"lastValidatedAt":{"type":["string","null"],"example":"2026-05-26T09:14:02.000Z"},"repoCount":{"type":"integer","example":3},"createdAt":{"type":"string","example":"2026-04-01T12:00:00.000Z"},"updatedAt":{"type":"string","example":"2026-05-26T09:14:02.000Z"}},"required":["id","name","provider","apiUrl","isValid","lastValidatedAt","repoCount","createdAt","updatedAt"],"description":"A workspace SCM connection. The stored token is never returned."},"ScmConnectionListResponse":{"type":"object","properties":{"connections":{"type":"array","items":{"$ref":"#/components/schemas/ScmConnection"}}},"required":["connections"]},"ScmConnectionResponse":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/ScmConnection"}},"required":["connection"]},"CreateScmConnectionBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[\\w\\s.:-]+$","example":"Company GitLab"},"provider":{"type":"string","enum":["gitlab","gitea","github","forgejo"],"example":"gitlab"},"apiUrl":{"type":"string","minLength":1,"maxLength":500,"format":"uri","example":"https://gitlab.example.com"},"accessToken":{"type":"string","minLength":1,"maxLength":500,"example":"glpat-…"}},"required":["name","provider","apiUrl","accessToken"]},"DiscoverReposResponse":{"type":"object","properties":{"valid":{"type":"boolean","example":true},"projects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"group/project"},"name":{"type":["string","null"],"example":"project"},"nameWithNamespace":{"type":"string","example":"group / project"},"pathWithNamespace":{"type":"string","example":"group/project"},"defaultBranch":{"type":["string","null"],"example":"main"},"webUrl":{"type":["string","null"],"example":"https://gitlab.example.com/group/project"},"description":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]}},"required":["id","name","nameWithNamespace","pathWithNamespace","defaultBranch","webUrl","description","avatarUrl"]}},"totalPages":{"type":"integer","example":1},"currentPage":{"type":"integer","example":1},"error":{"type":"string","description":"Present with valid:false when the token probe failed."}},"required":["valid"]},"Trigger":{"type":"object","properties":{"id":{"type":"string","example":"trg_1c2d3e4f"},"name":{"type":"string","example":"push to main"},"type":{"type":"string","description":"webhook | schedule | manual.","example":"webhook"},"enabled":{"type":"boolean","example":true},"allowManualRun":{"type":"boolean","example":true},"configSourceType":{"type":"string","description":"inline | repo_file.","example":"repo_file"},"webhookProvider":{"type":["string","null"],"example":"gitlab"},"eventRepository":{"type":["object","null"],"properties":{"id":{"type":"string","example":"repo_8d31aa02"},"fullName":{"type":"string","example":"roo-cloud/loha"},"provider":{"type":"string","example":"gitlab"}},"required":["id","fullName","provider"]},"lastInvokedAt":{"type":["string","null"],"example":"2026-05-26T09:14:02.000Z"},"invocationCount":{"type":"integer","example":18},"scheduleCron":{"type":["string","null"],"example":"0 2 * * *"},"scheduleTimezone":{"type":["string","null"],"example":"Asia/Bangkok"},"scheduleNextRunAt":{"type":["string","null"],"example":"2026-05-27T19:00:00.000Z"},"scheduleLastFiredAt":{"type":["string","null"],"example":"2026-05-26T19:00:00.000Z"},"scheduleSkipIfUnchanged":{"type":"boolean","example":false},"createdAt":{"type":"string","example":"2026-04-01T12:00:00.000Z"},"updatedAt":{"type":"string","example":"2026-05-26T09:14:02.000Z"}},"required":["id","name","type","enabled","allowManualRun","configSourceType","webhookProvider","eventRepository","lastInvokedAt","invocationCount","scheduleCron","scheduleTimezone","scheduleNextRunAt","scheduleLastFiredAt","scheduleSkipIfUnchanged","createdAt","updatedAt"],"description":"A pipeline trigger (webhook, schedule, or manual)."},"TriggerListResponse":{"type":"object","properties":{"triggers":{"type":"array","items":{"$ref":"#/components/schemas/Trigger"}}},"required":["triggers"]},"CreateTriggerResponse":{"type":"object","properties":{"trigger":{"type":"object","properties":{"id":{"type":"string","example":"trg_1c2d3e4f"},"name":{"type":"string","example":"deploy main"},"type":{"type":"string","example":"manual"},"configSourceType":{"type":"string","example":"repo_file"},"scheduleCron":{"type":["string","null"],"example":"0 2 * * *"},"scheduleTimezone":{"type":["string","null"],"example":"Asia/Bangkok"},"scheduleNextRunAt":{"type":["string","null"],"example":"2026-05-27T19:00:00.000Z"},"scheduleLastFiredAt":{"type":["string","null"],"example":"2026-05-26T19:00:00.000Z"},"scheduleSkipIfUnchanged":{"type":"boolean","example":false}},"required":["id","name","type","configSourceType","scheduleCron","scheduleTimezone","scheduleNextRunAt","scheduleLastFiredAt","scheduleSkipIfUnchanged"]}},"required":["trigger"]},"CreateTriggerBody":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":80,"example":"deploy main"},"description":{"type":"string","maxLength":280},"type":{"type":"string","enum":["manual","webhook","schedule"],"example":"manual"},"scheduleCron":{"type":"string","minLength":9,"maxLength":100,"description":"Five-field cron, evaluated in Asia/Bangkok. Required for (and only allowed on) type \"schedule\".","example":"0 2 * * *"},"scheduleSkipIfUnchanged":{"type":"boolean","description":"Skip a scheduled run when the branch has no new commits since the last scheduled run. Needs a repo_file source.","example":false},"eventFilters":{"type":"object","properties":{"events":{"type":"array","items":{"type":"string","enum":["push","pull_request","tag"]},"maxItems":20},"branches":{"type":"array","items":{"type":"string","maxLength":256},"maxItems":20},"pathFilters":{"type":"array","items":{"type":"string","maxLength":256},"maxItems":20},"tags":{"type":"array","items":{"type":"string","maxLength":64},"maxItems":20}},"description":"Optional ingress filters reconciled against inline configs."},"source":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["inline"]},"yaml":{"type":"string","minLength":1,"maxLength":262144}},"required":["kind","yaml"]},{"type":"object","properties":{"kind":{"type":"string","enum":["repo_file"]},"repositoryId":{"type":"string","minLength":1},"filePath":{"type":"string","minLength":1,"maxLength":512},"branch":{"type":"string","minLength":1,"maxLength":256}},"required":["kind","repositoryId"]}],"description":"Where the pipeline config comes from."}},"required":["name","source"],"additionalProperties":false},"RunResponse":{"type":"object","properties":{"run":{"type":"object","properties":{"id":{"type":"string","example":"bld_2a7f1c9e4b"},"runNumber":{"type":"integer","example":43},"status":{"type":"string","example":"PENDING"}},"required":["id","status"]},"dispatch":{"type":"object","properties":{"pipelineRunName":{"type":"string","description":"Set on a successful dispatch.","example":"loha-run-2a7f1c9e4b"},"error":{"type":"string","example":"controller unreachable"},"code":{"type":"string","example":"CONTROLLER_UNREACHABLE"},"blocked":{"type":"boolean"},"verificationNeeded":{"type":"boolean"}}}},"required":["run","dispatch"],"description":"The recorded run plus its dispatch outcome."},"RunBody":{"type":"object","properties":{"branch":{"type":"string","minLength":1,"maxLength":256,"example":"main"},"commitSha":{"type":"string","maxLength":64,"example":"9f3c1b2d4e5a6f7081920a3b4c5d6e7f80123456"},"parameters":{"type":"object","additionalProperties":{"type":"string","maxLength":4096},"example":{"DEPLOY_ENV":"production"}}},"additionalProperties":false,"description":"Optional overrides for a manual run."},"Webhook":{"type":"object","properties":{"id":{"type":"string","example":"whk_3f2e1d0c"},"workspaceId":{"type":"string","example":"org_3c4d5e6f"},"triggerId":{"type":["string","null"],"example":"trg_1c2d3e4f"},"kind":{"type":"string","enum":["generic","msteams","line"],"example":"generic"},"url":{"type":["string","null"],"example":"https://example.com/hooks/loha"},"eventTypes":{"type":"array","items":{"type":"string"},"example":["com.loha.build.completed.v1"]},"statusFilter":{"type":["array","null"],"items":{"type":"string"},"example":["SUCCESS","FAILED"]},"enabled":{"type":"boolean","example":true},"description":{"type":["string","null"],"example":"Notify deploy bot"},"disabledReason":{"type":["string","null"],"example":null},"secretRotatedAt":{"type":["string","null"],"example":"2026-05-26T09:14:02.000Z"},"createdAt":{"type":"string","example":"2026-04-01T12:00:00.000Z"},"updatedAt":{"type":"string","example":"2026-05-26T09:14:02.000Z"}},"required":["id","workspaceId","triggerId","kind","url","eventTypes","statusFilter","enabled","description","disabledReason","secretRotatedAt","createdAt","updatedAt"],"additionalProperties":false,"description":"An outbound webhook subscription. Never includes the secret."},"WebhookListResponse":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}},"required":["webhooks"]},"CreateWebhookResponse":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"},"secret":{"type":["string","null"],"description":"The signing secret — returned ONLY here, once. Store it now. `null` for a delivery kind that signs nothing (e.g. Microsoft Teams), where the endpoint URL is itself the credential.","example":"whsec_…"}},"required":["webhook","secret"]},"CreateWebhookBody":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri","example":"https://example.com/hooks/loha"},"eventTypes":{"type":"array","items":{"type":"string","enum":["com.loha.build.completed.v1","com.loha.release.produced.v1","com.loha.usage.period.closed.v1","com.loha.security.issue.new_critical.v1","com.loha.security.issue.sla_breached.v1","com.loha.security.issue.secret_detected.v1"]},"minItems":1},"statusFilter":{"type":"array","items":{"type":"string","enum":["SUCCESS","FAILED","TIMEOUT","CANCELLED"]},"minItems":1},"triggerId":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"description":{"type":"string","maxLength":280}},"required":["url"],"additionalProperties":false},"ValidateSuccessResponse":{"type":"object","properties":{"valid":{"type":"boolean","enum":[true],"example":true},"config":{"type":"object","additionalProperties":{},"description":"The parsed, normalized pipeline config."},"warnings":{"type":"array","items":{"type":"string"},"description":"Non-fatal advisories. The document is valid and will run as-is. Currently emitted for a `uses:` reference with no major-version pin, which resolves to the newest major and so can change behaviour when a new major ships.","example":["jobs.build.steps[0].uses: Task reference 'loha/git-clone' has no version pin and resolves to 'loha/git-clone@v1' (the newest major). Pin it so a future major does not change this step's behaviour."]}},"required":["valid","config"]},"ValidateErrorResponse":{"type":"object","properties":{"valid":{"type":"boolean","enum":[false],"example":false},"errors":{"type":"array","items":{"type":"string"},"example":["pipeline.steps[0].name is required"]}},"required":["valid","errors"]},"ValidatePipelineBody":{"type":"object","properties":{"yaml":{"type":"string","description":"Raw .loha.yml document.","example":"version: \"1\"\npipeline:\n  steps:\n    - name: build\n      run: npm ci && npm run build\n"}},"required":["yaml"]},"PlanEnvVar":{"type":"object","properties":{"name":{"type":"string","example":"CI"},"value":{"type":["string","null"],"example":"true"},"masked":{"type":"boolean","example":false}},"required":["name","value","masked"]},"PlanService":{"type":"object","properties":{"name":{"type":"string","example":"postgres"},"image":{"type":"string","example":"postgres:16"},"env":{"type":"array","items":{"$ref":"#/components/schemas/PlanEnvVar"}},"ports":{"type":"array","items":{"type":"integer"},"example":[5432]},"aliases":{"type":"array","items":{"type":"string"},"example":["db"]},"command":{"type":["array","null"],"items":{"type":"string"}},"args":{"type":["array","null"],"items":{"type":"string"}},"user":{"type":["object","null"],"properties":{"uid":{"type":"integer"},"gid":{"type":"integer"}},"required":["uid","gid"]},"capAdd":{"type":"array","items":{"type":"string"}},"resources":{"type":"object","properties":{"cpu":{"type":["string","null"]},"memory":{"type":["string","null"]}},"required":["cpu","memory"]}},"required":["name","image","env","ports","aliases","command","args","user","capAdd","resources"]},"PlanStepSource":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["run"]}},"required":["kind"]},{"type":"object","properties":{"kind":{"type":"string","enum":["task"]},"uses":{"type":"string","example":"loha/run-tests@v1"},"taskName":{"type":"string","example":"run-tests"},"version":{"type":"string","example":"1.2.0"}},"required":["kind","uses","taskName","version"]},{"type":"object","properties":{"kind":{"type":"string","enum":["platform"]},"name":{"type":"string","example":"approval"}},"required":["kind","name"]}]},"PlanContainer":{"type":"object","properties":{"name":{"type":"string","example":"run"},"image":{"type":"string","example":"node:22"},"script":{"type":["string","null"]},"command":{"type":["array","null"],"items":{"type":"string"}},"args":{"type":["array","null"],"items":{"type":"string"}},"workingDir":{"type":"string","example":"/workspace/source"},"user":{"type":["object","null"],"properties":{"uid":{"type":"integer"},"gid":{"type":"integer"}},"required":["uid","gid"]},"capAdd":{"type":"array","items":{"type":"string"}},"scratchMounts":{"type":"array","items":{"type":"string"}},"resultsDir":{"type":"string","example":"/loha/results/unit-tests"},"env":{"type":"array","items":{"$ref":"#/components/schemas/PlanEnvVar"}},"secretEnv":{"type":"array","items":{"type":"string"}},"platformEnv":{"type":"array","items":{"type":"string"}},"network":{"type":"string","enum":["services","none"]},"resources":{"type":"object","properties":{"cpu":{"type":["string","null"]},"memory":{"type":["string","null"]}},"required":["cpu","memory"]}},"required":["name","image","script","command","args","workingDir","user","capAdd","scratchMounts","resultsDir","env","secretEnv","platformEnv","network","resources"]},"PlanStep":{"type":"object","properties":{"name":{"type":"string","example":"unit-tests"},"source":{"$ref":"#/components/schemas/PlanStepSource"},"enabled":{"type":"boolean"},"skipReason":{"type":["string","null"]},"local":{"type":"object","properties":{"mode":{"type":"string","enum":["run","emulated","skipped"]},"reason":{"type":"string"}},"required":["mode","reason"]},"containers":{"type":"array","items":{"$ref":"#/components/schemas/PlanContainer"}},"timeoutSeconds":{"type":["integer","null"]},"retries":{"type":"integer"}},"required":["name","source","enabled","skipReason","local","containers","timeoutSeconds","retries"]},"PlanJob":{"type":"object","properties":{"id":{"type":"string","example":"test"},"authoredId":{"type":"string","example":"test"},"matrix":{"type":["object","null"],"additionalProperties":{"type":"string"}},"needs":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"skipReason":{"type":["string","null"]},"environment":{"type":["string","null"]},"approval":{"type":["object","null"],"properties":{"message":{"type":["string","null"]},"timeoutSeconds":{"type":"integer"}},"required":["message","timeoutSeconds"]},"jobArtifacts":{"type":["object","null"],"properties":{"saveStepName":{"type":["string","null"],"example":"build-artifacts-save"},"restoreFrom":{"type":"array","items":{"type":"string"},"example":["build"]}},"required":["saveStepName","restoreFrom"]},"resources":{"type":"object","properties":{"class":{"type":"string","example":"medium"},"cpu":{"type":["string","null"]},"memory":{"type":["string","null"]}},"required":["class","cpu","memory"]},"services":{"type":"array","items":{"$ref":"#/components/schemas/PlanService"}},"serviceWait":{"type":["object","null"],"properties":{"script":{"type":"string"},"timeoutSeconds":{"type":"integer"}},"required":["script","timeoutSeconds"]},"steps":{"type":"array","items":{"$ref":"#/components/schemas/PlanStep"}}},"required":["id","authoredId","matrix","needs","enabled","skipReason","environment","approval","jobArtifacts","resources","services","serviceWait","steps"]},"BuildPlan":{"type":"object","properties":{"planVersion":{"type":"number","enum":[1]},"context":{"type":"object","properties":{"branch":{"type":"string"},"event":{"type":"string"},"commitSha":{"type":"string"},"repositoryId":{"type":["string","null"]},"triggerId":{"type":["string","null"]},"triggerSelection":{"type":"string","enum":["explicit","auto","none"]},"trigger":{"type":["object","null"],"properties":{"fires":{"type":"boolean"},"reason":{"type":"string","example":"Branch \"main\" matches on.push.branches"}},"required":["fires","reason"]}},"required":["branch","event","commitSha","repositoryId","triggerId","triggerSelection","trigger"]},"cluster":{"type":"object","properties":{"arch":{"type":"string","example":"amd64"},"workdir":{"type":"string","example":"/workspace/source"},"runAsUser":{"type":"integer"},"runAsGroup":{"type":"integer"}},"required":["arch","workdir","runAsUser","runAsGroup"]},"timeoutSeconds":{"type":"integer"},"variables":{"type":"object","properties":{"valuesIncluded":{"type":"boolean"}},"required":["valuesIncluded"]},"warnings":{"type":"array","items":{"type":"string"}},"jobs":{"type":"array","items":{"$ref":"#/components/schemas/PlanJob"}}},"required":["planVersion","context","cluster","timeoutSeconds","variables","warnings","jobs"]},"PlanSuccessResponse":{"type":"object","properties":{"plan":{"$ref":"#/components/schemas/BuildPlan"}},"required":["plan"]},"PlanRequestBody":{"type":"object","properties":{"yaml":{"type":"string","maxLength":1048576,"description":"Raw .loha.yml document (at most 1 MiB)."},"filePath":{"type":"string","default":".loha.yml","example":".loha.yml"},"repositoryId":{"type":"string"},"triggerId":{"type":"string"},"branch":{"type":"string","minLength":1,"example":"main"},"event":{"type":"string","enum":["push","pull_request","tag_push","manual","release"]},"commitSha":{"type":"string","minLength":7,"example":"a1b2c3d"},"commitMessage":{"type":"string"},"runtimeVars":{"type":"object","additionalProperties":{"type":"string"}},"targetBranch":{"type":"string","minLength":1,"example":"main"},"tag":{"type":"string","minLength":1,"example":"v1.2.3"}},"required":["yaml","branch","event","commitSha"]},"WebhookDetailResponse":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}},"required":["webhook"]},"UpdateWebhookBody":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"eventTypes":{"type":"array","items":{"type":"string","enum":["com.loha.build.completed.v1","com.loha.release.produced.v1","com.loha.usage.period.closed.v1","com.loha.security.issue.new_critical.v1","com.loha.security.issue.sla_breached.v1","com.loha.security.issue.secret_detected.v1"]},"minItems":1},"statusFilter":{"type":["array","null"],"items":{"type":"string","enum":["SUCCESS","FAILED","TIMEOUT","CANCELLED"]},"minItems":1},"triggerId":{"type":["string","null"],"minLength":1},"description":{"type":["string","null"],"maxLength":280},"enabled":{"type":"boolean"}},"additionalProperties":false},"DeleteWebhookResponse":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"]},"PingWebhookResponse":{"type":"object","properties":{"queued":{"type":"boolean","enum":[true]},"deliveryId":{"type":"string","example":"whd_7a6b5c4d"}},"required":["queued","deliveryId"]},"RotateWebhookSecretResponse":{"type":"object","properties":{"secret":{"type":"string","description":"The new signing secret. The previous secret stays valid for a 7-day overlap.","example":"whsec_…"}},"required":["secret"]},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","example":"whd_7a6b5c4d"},"eventId":{"type":"string","example":"evt_1a2b3c4d"},"eventType":{"type":"string","example":"com.loha.build.completed.v1"},"status":{"type":"string","description":"pending | delivered | failed.","example":"delivered"},"attempts":{"type":"integer","example":1},"responseStatus":{"type":["integer","null"],"example":200},"lastError":{"type":["string","null"],"example":null},"createdAt":{"type":"string","example":"2026-05-26T09:14:02.000Z"},"lastAttemptAt":{"type":["string","null"],"example":"2026-05-26T09:14:03.000Z"},"deliveredAt":{"type":["string","null"],"example":"2026-05-26T09:14:03.500Z"},"nextAttemptAt":{"type":["string","null"],"example":null}},"required":["id","eventId","eventType","status","attempts","responseStatus","lastError","createdAt","lastAttemptAt","deliveredAt","nextAttemptAt"],"additionalProperties":false,"description":"A delivery attempt. Excludes the stored payload."},"WebhookDeliveryListResponse":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}},"required":["deliveries"]},"RedeliverWebhookResponse":{"type":"object","properties":{"requeued":{"type":"boolean","enum":[true]}},"required":["requeued"]}},"parameters":{}},"paths":{"/api/v1/whoami":{"get":{"summary":"Resolve the token and its workspace","description":"Verify a token and return the bound workspace, scopes, and principal. No scope required.","security":[{"bearerToken":[]}],"responses":{"200":{"description":"Token identity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoamiResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/builds":{"get":{"summary":"List builds","description":"Scope: `builds:read`. Returns a page of builds in the token's workspace, newest first.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"repo_8d31aa02"},"required":false,"name":"repositoryId","in":"query"},{"schema":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"required":false,"name":"triggerId","in":"query"},{"schema":{"type":"string","description":"Filter by build status (case-insensitive).","example":"SUCCESS"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","minLength":1,"example":"main"},"required":false,"name":"branch","in":"query"},{"schema":{"type":"string","minLength":1,"example":"v1.2.0"},"required":false,"name":"tag","in":"query"},{"schema":{"type":"string","minLength":1,"example":"env=prod"},"required":false,"name":"label","in":"query"},{"schema":{"type":"string","description":"ISO-8601 lower bound on createdAt.","example":"2026-05-01T00:00:00.000Z"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","description":"ISO-8601 upper bound on createdAt.","example":"2026-05-31T23:59:59.000Z"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string","enum":["createdAt","duration","status"],"example":"createdAt"},"required":false,"name":"orderBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"],"example":"desc"},"required":false,"name":"dir","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Default 50, max 100.","example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"description":"Default 0.","example":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"A page of builds","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildListResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/builds/{id}":{"get":{"summary":"Get a build","description":"Scope: `builds:read`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Build detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildDetailResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"summary":"Cancel a build","description":"Scope: `builds:write`. Cancels a PENDING/QUEUED/RUNNING build.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBuildResponse"}}}},"202":{"description":"Cancellation requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBuildResponse"}}}},"400":{"description":"Build already completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Build status changed concurrently","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/builds/{id}/security":{"get":{"summary":"Read build security evidence","description":"Scope: `security:read`. Sanitized assessment and exact Sonar analysis, or assessment:null when not evaluated. Pipeline-uploaded evidence is informational.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Security assessment","content":{"application/json":{"schema":{"type":"object","properties":{"assessment":{"type":["object","null"],"properties":{"id":{"type":"string"},"buildId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"repositoryId":{"type":["string","null"]},"state":{"type":"string","enum":["pending","completed","cancelled","error"]},"policyRevision":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"scans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"scanner":{"type":"string","enum":["trivy","sonar"]},"targetKind":{"type":"string","enum":["source","image"]},"targetIdentity":{"type":"string"},"attempt":{"type":"integer","exclusiveMinimum":0},"origin":{"type":"string","enum":["pipeline_uploaded","loha_managed","sonar_correlated"]},"state":{"type":"string","enum":["queued","running","completed","error","cancelled","unsupported"]},"errorCode":{"type":["string","null"]},"startedAt":{"type":["string","null"],"format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time"},"report":{"type":["object","null"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["available","deleting","expired"]},"findingCount":{"type":"integer","minimum":0},"summary":{"type":"object","properties":{"total":{"type":"integer","minimum":0},"byCategory":{"type":"object","properties":{"vulnerability":{"type":"integer","minimum":0},"secret":{"type":"integer","minimum":0},"misconfiguration":{"type":"integer","minimum":0},"license":{"type":"integer","minimum":0},"quality":{"type":"integer","minimum":0}}},"bySeverity":{"type":"object","properties":{"UNKNOWN":{"type":"integer","minimum":0},"LOW":{"type":"integer","minimum":0},"MEDIUM":{"type":"integer","minimum":0},"HIGH":{"type":"integer","minimum":0},"CRITICAL":{"type":"integer","minimum":0}}}},"required":["total","byCategory","bySeverity"]},"expiresAt":{"type":"string","format":"date-time"}},"required":["id","state","findingCount","summary","expiresAt"]},"sonarAnalysis":{"type":["object","null"],"properties":{"state":{"type":"string","enum":["pending","completed","error","cancelled","unsupported","stale"]},"analysisId":{"type":["string","null"],"maxLength":512},"qualityGate":{"type":["string","null"],"enum":["passed","failed","unknown"]},"conditions":{"type":"array","items":{"type":"object","properties":{"metric":{"type":"string","minLength":1,"maxLength":200},"comparator":{"type":["string","null"],"maxLength":64},"errorThreshold":{"type":["string","null"],"maxLength":256},"actualValue":{"type":["string","null"],"maxLength":256},"status":{"type":"string","enum":["OK","ERROR","NO_VALUE","UNKNOWN"]}},"required":["metric","comparator","errorThreshold","actualValue","status"]},"maxItems":100},"conditionsFingerprint":{"type":["string","null"],"maxLength":256},"expectedGateMatched":{"type":["boolean","null"]},"dashboardUrl":{"type":["string","null"],"format":"uri"},"completedAt":{"type":["string","null"],"format":"date-time"}},"required":["state","analysisId","qualityGate","conditions","conditionsFingerprint","expectedGateMatched","dashboardUrl","completedAt"]}},"required":["id","scanner","targetKind","targetIdentity","attempt","origin","state","errorCode","startedAt","completedAt","report"]}}},"required":["id","buildId","projectId","repositoryId","state","policyRevision","createdAt","scans"]},"promotions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"buildId":{"type":["string","null"]},"taskName":{"type":"string"},"imageRepository":{"type":"string"},"requestedTag":{"type":"string"},"quarantineTag":{"type":"string"},"imageDigest":{"type":["string","null"],"pattern":"^sha256:[a-f0-9]{64}$"},"state":{"type":"string"},"errorCode":{"type":["string","null"]},"attempt":{"type":"integer"},"promotedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","buildId","taskName","imageRepository","requestedTag","quarantineTag","imageDigest","state","errorCode","attempt","promotedAt","createdAt","updatedAt"]}}},"required":["assessment"]}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Build not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/security/scans/{id}/findings":{"get":{"summary":"List sanitized security findings","description":"Scope: `security:read`. Tenant and project access are checked for retained evidence.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":2048},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","enum":["UNKNOWN","LOW","MEDIUM","HIGH","CRITICAL"]},"required":false,"name":"severity","in":"query"},{"schema":{"type":"string","enum":["vulnerability","secret","misconfiguration","license","quality"]},"required":false,"name":"category","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Findings page","content":{"application/json":{"schema":{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fingerprint":{"type":"string"},"scanner":{"type":"string","enum":["trivy","sonar"]},"target":{"type":"string","enum":["source","image"]},"targetIdentity":{"type":"string"},"category":{"type":"string","enum":["vulnerability","secret","misconfiguration","license","quality"]},"severity":{"type":"string","enum":["UNKNOWN","LOW","MEDIUM","HIGH","CRITICAL"]},"ruleId":{"type":["string","null"]},"packageName":{"type":["string","null"]},"installedVersion":{"type":["string","null"]},"fixedVersion":{"type":["string","null"]},"title":{"type":["string","null"]},"path":{"type":["string","null"]},"startLine":{"type":["integer","null"],"exclusiveMinimum":0},"endLine":{"type":["integer","null"],"exclusiveMinimum":0},"remediationUrl":{"type":["string","null"],"format":"uri"},"issue":{"type":["object","null"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["open","fixed","reopened","false_positive","accepted_risk"]}},"required":["id","status"]}},"required":["id","fingerprint","scanner","target","targetIdentity","category","severity","ruleId","packageName","installedVersion","fixedVersion","title","path","startLine","endLine","remediationUrl"]}},"nextCursor":{"type":["string","null"]}},"required":["findings","nextCursor"]}}}},"400":{"description":"Invalid filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scan not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"410":{"description":"Evidence expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/builds/{id}/security/rescans":{"post":{"summary":"Request a managed security rescan","description":"Scope: `security:rescan`. No request body or query. Targets and scanner profiles are reconstructed by the server from the immutable assessment. Accepted means queued/running, not passed.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"}],"responses":{"202":{"description":"Managed rescan accepted","content":{"application/json":{"schema":{"type":"object","properties":{"accepted":{"type":"boolean","enum":[true]},"scanId":{"type":"string","minLength":1,"maxLength":256},"state":{"type":"string","enum":["queued","running"]}},"required":["accepted","scanId","state"]}}}},"400":{"description":"Unexpected body or query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Build not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Build cannot be rescanned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/security/issues":{"get":{"summary":"List tracked security issues","description":"Scope: `security:read`. Issues persist across builds and report retention. Sorted most severe first; page with `cursor`. 404 while the workspace does not have security issues enabled.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"anyOf":[{"type":"string","enum":["open","fixed","reopened","false_positive","accepted_risk"]},{"type":"string","enum":["active"]}]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","enum":["UNKNOWN","LOW","MEDIUM","HIGH","CRITICAL"]},"required":false,"name":"severity","in":"query"},{"schema":{"type":"string","enum":["vulnerability","secret","misconfiguration","license"]},"required":false,"name":"category","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":256},"required":false,"name":"repositoryId","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":256},"required":false,"name":"owner","in":"query"},{"schema":{"type":"string","enum":["true"]},"required":false,"name":"slaBreached","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":512},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Issues page","content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"repositoryId":{"type":["string","null"]},"repositoryName":{"type":["string","null"]},"projectId":{"type":["string","null"]},"target":{"type":"string","enum":["source","image"]},"category":{"type":"string","enum":["vulnerability","secret","misconfiguration","license"]},"severity":{"type":"string","enum":["UNKNOWN","LOW","MEDIUM","HIGH","CRITICAL"]},"title":{"type":["string","null"]},"ruleId":{"type":["string","null"]},"packageName":{"type":["string","null"]},"path":{"type":["string","null"]},"imageRepository":{"type":["string","null"]},"fixedVersion":{"type":["string","null"]},"status":{"type":"string","enum":["open","fixed","reopened","false_positive","accepted_risk"]},"ownerUserId":{"type":["string","null"]},"ticketUrl":{"type":["string","null"],"format":"uri"},"triageProposal":{"type":["object","null"],"properties":{"status":{"type":"string","enum":["false_positive","accepted_risk"]},"reason":{"type":"string"},"expiresAt":{"type":["string","null"],"format":"date-time"},"proposedBy":{"type":"string"},"proposedAt":{"type":"string","format":"date-time"},"requiresIndependentApprover":{"type":"boolean"}},"required":["status","reason","expiresAt","proposedBy","proposedAt","requiresIndependentApprover"]},"triageExpiresAt":{"type":["string","null"],"format":"date-time"},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"fixedAt":{"type":["string","null"],"format":"date-time"},"slaDueAt":{"type":["string","null"],"format":"date-time"},"slaBreachedAt":{"type":["string","null"],"format":"date-time"},"overdue":{"type":"boolean"},"version":{"type":"integer","minimum":0}},"required":["id","repositoryId","repositoryName","projectId","target","category","severity","title","ruleId","packageName","path","imageRepository","fixedVersion","status","ownerUserId","ticketUrl","triageProposal","triageExpiresAt","firstSeenAt","lastSeenAt","fixedAt","slaDueAt","slaBreachedAt","overdue","version"]}},"nextCursor":{"type":["string","null"]}},"required":["issues","nextCursor"]}}}},"400":{"description":"Invalid filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Security issues not enabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/security/issues/{id}":{"get":{"summary":"Read one security issue","description":"Scope: `security:read`. Latest 50 occurrences and 100 history events.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Issue detail","content":{"application/json":{"schema":{"type":"object","properties":{"issue":{"type":"object","properties":{"id":{"type":"string"},"repositoryId":{"type":["string","null"]},"repositoryName":{"type":["string","null"]},"projectId":{"type":["string","null"]},"target":{"type":"string","enum":["source","image"]},"category":{"type":"string","enum":["vulnerability","secret","misconfiguration","license"]},"severity":{"type":"string","enum":["UNKNOWN","LOW","MEDIUM","HIGH","CRITICAL"]},"title":{"type":["string","null"]},"ruleId":{"type":["string","null"]},"packageName":{"type":["string","null"]},"path":{"type":["string","null"]},"imageRepository":{"type":["string","null"]},"fixedVersion":{"type":["string","null"]},"status":{"type":"string","enum":["open","fixed","reopened","false_positive","accepted_risk"]},"ownerUserId":{"type":["string","null"]},"ticketUrl":{"type":["string","null"],"format":"uri"},"triageProposal":{"type":["object","null"],"properties":{"status":{"type":"string","enum":["false_positive","accepted_risk"]},"reason":{"type":"string"},"expiresAt":{"type":["string","null"],"format":"date-time"},"proposedBy":{"type":"string"},"proposedAt":{"type":"string","format":"date-time"},"requiresIndependentApprover":{"type":"boolean"}},"required":["status","reason","expiresAt","proposedBy","proposedAt","requiresIndependentApprover"]},"triageExpiresAt":{"type":["string","null"],"format":"date-time"},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"fixedAt":{"type":["string","null"],"format":"date-time"},"slaDueAt":{"type":["string","null"],"format":"date-time"},"slaBreachedAt":{"type":["string","null"],"format":"date-time"},"overdue":{"type":"boolean"},"version":{"type":"integer","minimum":0}},"required":["id","repositoryId","repositoryName","projectId","target","category","severity","title","ruleId","packageName","path","imageRepository","fixedVersion","status","ownerUserId","ticketUrl","triageProposal","triageExpiresAt","firstSeenAt","lastSeenAt","fixedAt","slaDueAt","slaBreachedAt","overdue","version"]},"occurrences":{"type":"array","items":{"type":"object","properties":{"assessmentId":{"type":"string"},"buildId":{"type":["string","null"]},"revision":{"type":["string","null"]},"imageDigest":{"type":["string","null"]},"pushBranch":{"type":["string","null"]},"observedAt":{"type":"string","format":"date-time"}},"required":["assessmentId","buildId","revision","imageDigest","observedAt"]}},"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["detected","fixed","reopened","triage_proposed","triage_rejected","triaged","triage_expired","assigned","ticket_linked","sla_breached","exception_requested","exception_approved","exception_revoked","fix_pr_opened"]},"fromStatus":{"type":["string","null"],"enum":["open","fixed","reopened","false_positive","accepted_risk"]},"toStatus":{"type":["string","null"],"enum":["open","fixed","reopened","false_positive","accepted_risk"]},"actor":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["user"]},"id":{"type":"string"}},"required":["type","id"]},{"type":"object","properties":{"type":{"type":"string","enum":["system"]},"reason":{"type":"string"}},"required":["type","reason"]}]},"reason":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","kind","fromStatus","toStatus","actor","reason","createdAt"]}}},"required":["issue","occurrences","events"]}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Issue not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/builds/{id}/logs":{"get":{"summary":"Get build logs (JSON)","description":"Scope: `builds:read`. For live tailing use the SSE stream at /api/v1/builds/events.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"bld_2a7f1c9e4b"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Restrict to a single step's logs.","example":"build"},"required":false,"name":"step","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":5000,"description":"Page size. Default 1000, max 5000.","example":1000},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"description":"Default 0.","example":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Build logs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildLogsResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/releases":{"get":{"summary":"List releases","description":"Scope: `repos:read`. Returns a page of release records in the token's workspace, newest first — each one a commit that produced a container image, via a build. Join `imageUrl` and `imageDigest` with `@` for a deployable, immutable reference. Read-only: Loha writes releases itself when a build pushes an image.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size. Default 50, max 100.","example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"description":"Default 0.","example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string","minLength":1,"example":"repo_8d31aa02"},"required":false,"name":"repositoryId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Restrict to one project. Ignored for a project-bound key, which is already confined to its own.","example":"prj_4c1e9a77"},"required":false,"name":"projectId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"The build whose images to list. A build that pushed two images has two releases.","example":"bld_2a7f1c9e4b"},"required":false,"name":"buildId","in":"query"}],"responses":{"200":{"description":"A page of releases","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseListResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/repos":{"get":{"summary":"List repositories","description":"Scope: `repos:read`. Pagination is returned only when a filter/pagination query param is supplied.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200,"description":"Case-insensitive match on fullName/htmlUrl.","example":"loha"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["gitlab","github","gitea","forgejo"],"example":"gitlab"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"example":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Repositories","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepoListResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"summary":"Import a repository","description":"Scope: `repos:write`. Connects a repo via a workspace SCM connection (`scmConnectionId` + `fullName`) or inline credentials. The CLI `loha repos add`.","security":[{"bearerToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRepoBody"}}}},"responses":{"200":{"description":"Repository connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepoResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"SCM connection not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Repository already connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/scm-connections":{"get":{"summary":"List SCM connections","description":"Scope: `connections:read`. Returns connection metadata — never the stored token.","security":[{"bearerToken":[]}],"responses":{"200":{"description":"SCM connections","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScmConnectionListResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"summary":"Create an SCM connection","description":"Scope: `connections:write`. Stores an encrypted SCM token after probing it. The CLI `loha connections add`.","security":[{"bearerToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScmConnectionBody"}}}},"responses":{"200":{"description":"Connection created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScmConnectionResponse"}}}},"400":{"description":"Invalid request body or token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/scm-connections/{id}/repos":{"get":{"summary":"Discover remote repositories","description":"Scope: `repos:read`. Browses repos reachable through a stored connection without creating records. The CLI `loha repos discover`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"scm_9a8b7c6d"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","maxLength":100,"example":"proj"},"required":false,"name":"search","in":"query"},{"schema":{"type":"integer","minimum":1,"example":1},"required":false,"name":"page","in":"query"}],"responses":{"200":{"description":"Discovered repositories","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoverReposResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"SCM connection not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/triggers":{"get":{"summary":"List triggers","description":"Scope: `pipelines:read`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Filter to triggers fired by a repo's events.","example":"repo_8d31aa02"},"required":false,"name":"repositoryId","in":"query"}],"responses":{"200":{"description":"Triggers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerListResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/triggers/create":{"post":{"summary":"Create a trigger","description":"Scope: `triggers:write`. Scaffolds a trigger for a repo — the CLI `loha link`.","security":[{"bearerToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTriggerBody"}}}},"responses":{"200":{"description":"Trigger created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTriggerResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Repository not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/triggers/{id}/run":{"post":{"summary":"Run a trigger (deploy)","description":"Scope: `builds:write`. Fires a manual run against a git ref — the CLI `deploy`/`run`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunBody"}}}},"responses":{"200":{"description":"Run recorded (may be soft-blocked — see dispatch)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunResponse"}}}},"202":{"description":"Dispatched; controller verification pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Trigger disabled, manual run disabled, missing scope, or workspace inactive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Trigger not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Pipeline controller unreachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunResponse"}}}}}}},"/api/v1/triggers/{id}/webhooks":{"get":{"summary":"List a trigger’s outbound webhooks","description":"Scope: `webhooks:read`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"required":false,"name":"triggerId","in":"query"}],"responses":{"200":{"description":"Webhooks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"summary":"Create an outbound webhook on a trigger","description":"Scope: `webhooks:write`. Returns the signing secret once.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookBody"}}}},"responses":{"201":{"description":"Webhook created (secret returned once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/pipelines/validate":{"post":{"summary":"Validate a .loha.yml","description":"Scope: `pipelines:read`. Returns the parsed config or a list of validation errors.","security":[{"bearerToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidatePipelineBody"}}}},"responses":{"200":{"description":"Valid config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateSuccessResponse"}}}},"400":{"description":"Malformed request body or oversized document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation errors","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/pipelines/plan":{"post":{"summary":"Plan a .loha.yml run","description":"Scope: `pipelines:read`. Returns the steps the cluster would run. Plain variable values need `variables:values:read`; secret values are never returned.","security":[{"bearerToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanRequestBody"}}}},"responses":{"200":{"description":"The build plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanSuccessResponse"}}}},"400":{"description":"Malformed request body (`VALIDATION_ERROR`), or the build path refused the run (its own `code`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, workspace access revoked, the project binding does not cover the repository, or local runs are not enabled (`FEATURE_DISABLED`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Repository or trigger not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation errors (same body as `/pipelines/validate`), or the plan could not be projected (`PLAN_PROJECTION_FAILED`)","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidateErrorResponse"},{"$ref":"#/components/schemas/ErrorResponse"}]}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The pipeline planner is unavailable (`CONTROLLER_UNAVAILABLE`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks":{"get":{"summary":"List outbound webhooks","description":"Scope: `webhooks:read`. The signing secret is never returned.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"trg_1c2d3e4f"},"required":false,"name":"triggerId","in":"query"}],"responses":{"200":{"description":"Webhooks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"summary":"Create an outbound webhook","description":"Scope: `webhooks:write`. Returns the signing secret once.","security":[{"bearerToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookBody"}}}},"responses":{"201":{"description":"Webhook created (secret returned once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks/{id}":{"get":{"summary":"Get an outbound webhook","description":"Scope: `webhooks:read`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDetailResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"summary":"Update an outbound webhook","description":"Scope: `webhooks:write`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookBody"}}}},"responses":{"200":{"description":"Updated webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDetailResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"summary":"Delete an outbound webhook","description":"Scope: `webhooks:write`.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteWebhookResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks/{id}/ping":{"post":{"summary":"Send a test delivery","description":"Scope: `webhooks:write`. Queues a synthetic delivery to the webhook.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"}],"responses":{"202":{"description":"Test delivery queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingWebhookResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks/{id}/rotate-secret":{"post":{"summary":"Rotate the signing secret","description":"Scope: `webhooks:write`. Returns the new secret once; the previous secret stays valid for a 7-day overlap.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"New secret (returned once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateWebhookSecretResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"summary":"List recent deliveries","description":"Scope: `webhooks:read`. Newest first. Excludes the stored payload.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Default 50, max 200.","example":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Deliveries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryListResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver":{"post":{"summary":"Redeliver a past delivery","description":"Scope: `webhooks:write`. Re-queues a previous delivery attempt.","security":[{"bearerToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"whk_3f2e1d0c"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"example":"whd_7a6b5c4d"},"required":true,"name":"deliveryId","in":"path"}],"responses":{"200":{"description":"Re-queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeliverWebhookResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks the required scope, or workspace access revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Delivery not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"webhooks":{}}